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 React, { FC, useCallback } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import classNames from 'classnames';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
||||
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
||||
|
@ -33,6 +34,7 @@ interface ItemsViewProps {
|
|||
parentId: ParentId;
|
||||
itemType: BaseItemKind[];
|
||||
collectionType?: CollectionType;
|
||||
isPaginationEnabled?: boolean;
|
||||
isBtnPlayAllEnabled?: boolean;
|
||||
isBtnQueueEnabled?: boolean;
|
||||
isBtnShuffleEnabled?: boolean;
|
||||
|
@ -48,6 +50,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
|||
viewType,
|
||||
parentId,
|
||||
collectionType,
|
||||
isPaginationEnabled = true,
|
||||
isBtnPlayAllEnabled = false,
|
||||
isBtnQueueEnabled = false,
|
||||
isBtnShuffleEnabled = false,
|
||||
|
@ -145,6 +148,18 @@ const ItemsView: FC<ItemsViewProps> = ({
|
|||
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
||||
} else if (viewType === LibraryTab.Artists) {
|
||||
cardOptions.lines = 1;
|
||||
cardOptions.showYear = false;
|
||||
} else if (viewType === LibraryTab.Channels) {
|
||||
cardOptions.shape = 'square';
|
||||
cardOptions.showDetailsMenu = true;
|
||||
cardOptions.showCurrentProgram = true;
|
||||
cardOptions.showCurrentProgramTime = true;
|
||||
} else if (viewType === LibraryTab.SeriesTimers) {
|
||||
cardOptions.defaultShape = 'portrait';
|
||||
cardOptions.preferThumb = 'auto';
|
||||
cardOptions.showSeriesTimerTime = true;
|
||||
cardOptions.showSeriesTimerChannel = true;
|
||||
cardOptions.lines = 3;
|
||||
}
|
||||
|
||||
return cardOptions;
|
||||
|
@ -188,15 +203,23 @@ const ItemsView: FC<ItemsViewProps> = ({
|
|||
ItemSortBy.SortName
|
||||
);
|
||||
|
||||
const itemsContainerClass = classNames(
|
||||
'centered padded-left padded-right padded-right-withalphapicker',
|
||||
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
||||
'vertical-list' :
|
||||
'vertical-wrap'
|
||||
);
|
||||
return (
|
||||
<Box>
|
||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
{isPaginationEnabled && (
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnPlayAllEnabled && (
|
||||
<PlayAllButton
|
||||
|
@ -263,22 +286,23 @@ const ItemsView: FC<ItemsViewProps> = ({
|
|||
<Loading />
|
||||
) : (
|
||||
<ItemsContainer
|
||||
className='centered padded-left padded-right padded-right-withalphapicker'
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
className={itemsContainerClass}
|
||||
parentId={parentId}
|
||||
reloadItems={refetch}
|
||||
getItemsHtml={getItemsHtml}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
</Box>
|
||||
{isPaginationEnabled && (
|
||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { FC } from 'react';
|
||||
import SuggestionsView from './SuggestionsView';
|
||||
import SuggestionsSectionView from './SuggestionsSectionView';
|
||||
import UpcomingView from './UpcomingView';
|
||||
import GenresView from './GenresView';
|
||||
import ItemsView from './ItemsView';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { ParentId } from 'types/library';
|
||||
import { LibraryTabContent } from 'types/libraryTabContent';
|
||||
import GuideView from './GuideView';
|
||||
import ProgramsSectionView from './ProgramsSectionView';
|
||||
|
||||
interface PageTabContentProps {
|
||||
parentId: ParentId;
|
||||
|
@ -15,18 +17,30 @@ interface PageTabContentProps {
|
|||
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||
return (
|
||||
<SuggestionsView
|
||||
<SuggestionsSectionView
|
||||
parentId={parentId}
|
||||
suggestionSectionViews={
|
||||
currentTab.sectionsType?.suggestionSectionsView
|
||||
sectionType={
|
||||
currentTab.sectionsView?.suggestionSections ?? []
|
||||
}
|
||||
isMovieRecommendations={
|
||||
currentTab.sectionsType?.isMovieRecommendations
|
||||
isMovieRecommendationEnabled={
|
||||
currentTab.sectionsView?.isMovieRecommendations
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
|
||||
return (
|
||||
<ProgramsSectionView
|
||||
parentId={parentId}
|
||||
sectionType={
|
||||
currentTab.sectionsView?.programSections ?? []
|
||||
}
|
||||
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Upcoming) {
|
||||
return <UpcomingView parentId={parentId} />;
|
||||
}
|
||||
|
@ -41,11 +55,16 @@ const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Guide) {
|
||||
return <GuideView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={currentTab.viewType}
|
||||
parentId={parentId}
|
||||
collectionType={currentTab.collectionType}
|
||||
isPaginationEnabled={currentTab.isPaginationEnabled}
|
||||
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
||||
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
||||
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
|
@ -10,8 +10,8 @@ import { LibraryViewSettings } from 'types/library';
|
|||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
interface PlayAllButtonProps {
|
||||
item: BaseItemDto | undefined;
|
||||
items: BaseItemDto[];
|
||||
item: BaseItemDto | null | undefined;
|
||||
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||
viewType: LibraryTab;
|
||||
hasFilters: boolean;
|
||||
libraryViewSettings: LibraryViewSettings
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
import { Section, SectionType } from 'types/sections';
|
||||
|
||||
interface ProgramsSectionViewProps {
|
||||
parentId: ParentId;
|
||||
sectionType: SectionType[];
|
||||
isUpcomingRecordingsEnabled: boolean | undefined
|
||||
}
|
||||
|
||||
const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
|
||||
parentId,
|
||||
sectionType,
|
||||
isUpcomingRecordingsEnabled = false
|
||||
}) => {
|
||||
const { isLoading, data: sectionsWithItems } = useGetProgramsSectionsWithItems(parentId, sectionType);
|
||||
const {
|
||||
isLoading: isUpcomingRecordingsLoading,
|
||||
data: upcomingRecordings
|
||||
} = useGetTimers(isUpcomingRecordingsEnabled);
|
||||
|
||||
if (isLoading || isUpcomingRecordingsLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!sectionsWithItems?.length && !upcomingRecordings?.length) {
|
||||
return (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>
|
||||
{globalize.translate('MessageNoItemsAvailable')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getRouteUrl = (section: Section) => {
|
||||
return appRouter.getRouteUrl('list', {
|
||||
serverId: window.ApiClient.serverId(),
|
||||
itemTypes: section.itemTypes,
|
||||
isAiring: section.parametersOptions?.isAiring,
|
||||
isMovie: section.parametersOptions?.isMovie,
|
||||
isSports: section.parametersOptions?.isSports,
|
||||
isKids: section.parametersOptions?.isKids,
|
||||
isNews: section.parametersOptions?.isNews,
|
||||
isSeries: section.parametersOptions?.isSeries
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{sectionsWithItems?.map(({ section, items }) => (
|
||||
<SectionContainer
|
||||
key={section.type}
|
||||
sectionTitle={globalize.translate(section.name)}
|
||||
items={items ?? []}
|
||||
url={getRouteUrl(section)}
|
||||
cardOptions={{
|
||||
...section.cardOptions
|
||||
}}
|
||||
/>
|
||||
|
||||
))}
|
||||
|
||||
{upcomingRecordings?.map((group) => (
|
||||
<SectionContainer
|
||||
key={group.name}
|
||||
sectionTitle={group.name}
|
||||
items={group.timerInfo ?? []}
|
||||
cardOptions={{
|
||||
shape: 'overflowBackdrop',
|
||||
showTitle: true,
|
||||
showParentTitleOrTitle: true,
|
||||
showAirTime: true,
|
||||
showAirEndTime: true,
|
||||
showChannelName: false,
|
||||
cardLayout: true,
|
||||
centerText: false,
|
||||
action: 'edit',
|
||||
cardFooterAside: 'none',
|
||||
preferThumb: true,
|
||||
coverImage: true,
|
||||
allowBottomPadding: false,
|
||||
overlayText: false,
|
||||
showChannelLogo: true
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgramsSectionView;
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import QueueIcon from '@mui/icons-material/Queue';
|
||||
|
@ -8,7 +8,7 @@ import globalize from 'scripts/globalize';
|
|||
|
||||
interface QueueButtonProps {
|
||||
item: BaseItemDto | undefined
|
||||
items: BaseItemDto[];
|
||||
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||
hasFilters: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SectionContainer from './SectionContainer';
|
||||
|
||||
interface RecommendationContainerProps {
|
||||
recommendation?: RecommendationDto;
|
||||
}
|
||||
|
||||
const RecommendationContainer: FC<RecommendationContainerProps> = ({
|
||||
recommendation = {}
|
||||
}) => {
|
||||
let title = '';
|
||||
|
||||
switch (recommendation.RecommendationType) {
|
||||
case RecommendationType.SimilarToRecentlyPlayed:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouWatched',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.SimilarToLikedItem:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouLike',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedDirector:
|
||||
title = globalize.translate(
|
||||
'RecommendationDirectedBy',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasActorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedActor:
|
||||
title = globalize.translate(
|
||||
'RecommendationStarring',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
sectionTitle={escapeHTML(title)}
|
||||
items={recommendation.Items ?? []}
|
||||
cardOptions={{
|
||||
shape: 'overflowPortrait',
|
||||
showYear: true,
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationContainer;
|
|
@ -1,46 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetMovieRecommendations } from 'hooks/useFetchItems';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import globalize from 'scripts/globalize';
|
||||
import RecommendationContainer from './RecommendationContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
|
||||
interface RecommendationItemsContainerProps {
|
||||
parentId?: ParentId;
|
||||
}
|
||||
|
||||
const RecommendationItemsContainer: FC<RecommendationItemsContainerProps> = ({
|
||||
parentId
|
||||
}) => {
|
||||
const { isLoading, data: movieRecommendationsItems } =
|
||||
useGetMovieRecommendations(parentId);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
if (!movieRecommendationsItems?.length) {
|
||||
return (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>
|
||||
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{movieRecommendationsItems.map((recommendation, index) => {
|
||||
return (
|
||||
<RecommendationContainer
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||
recommendation={recommendation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationItemsContainer;
|
|
@ -1,8 +1,8 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
||||
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
||||
import Scroller from 'elements/emby-scroller/Scroller';
|
||||
import LinkButton from 'elements/emby-button/LinkButton';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
|
@ -12,7 +12,7 @@ import { CardOptions } from 'types/cardOptions';
|
|||
interface SectionContainerProps {
|
||||
url?: string;
|
||||
sectionTitle: string;
|
||||
items: BaseItemDto[];
|
||||
items: BaseItemDto[] | TimerInfoDto[];
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,9 @@ const SectionContainer: FC<SectionContainerProps> = ({
|
|||
isMouseWheelEnabled={false}
|
||||
isCenterFocusEnabled={true}
|
||||
>
|
||||
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
|
||||
<ItemsContainer
|
||||
className='itemsContainer scrollSlider focuscontainer-x'
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
@ -11,8 +11,8 @@ import { LibraryViewSettings } from 'types/library';
|
|||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
interface ShuffleButtonProps {
|
||||
item: BaseItemDto | undefined;
|
||||
items: BaseItemDto[];
|
||||
item: BaseItemDto | null | undefined;
|
||||
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||
viewType: LibraryTab
|
||||
hasFilters: boolean;
|
||||
libraryViewSettings: LibraryViewSettings
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||
import React, { FC } from 'react';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
||||
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
||||
import { ParentId } from 'types/library';
|
||||
|
||||
const getSuggestionsSections = (): Sections[] => {
|
||||
return [
|
||||
{
|
||||
name: 'HeaderContinueWatching',
|
||||
viewType: SectionsViewType.ResumeItems,
|
||||
type: 'Movie',
|
||||
view: SectionsView.ContinueWatchingMovies,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Movie]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
preferThumb: true,
|
||||
shape: 'overflowBackdrop',
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestMovies',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Movie',
|
||||
view: SectionsView.LatestMovies,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Movie]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowPortrait',
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderContinueWatching',
|
||||
viewType: SectionsViewType.ResumeItems,
|
||||
type: 'Episode',
|
||||
view: SectionsView.ContinueWatchingEpisode,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Episode]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
inheritThumb:
|
||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||
showYear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestEpisodes',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Episode',
|
||||
view: SectionsView.LatestEpisode,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Episode]
|
||||
},
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
showSeriesYear: true,
|
||||
showParentTitle: true,
|
||||
overlayText: false,
|
||||
showUnplayedIndicator: false,
|
||||
showChildCountIndicator: true,
|
||||
lazy: true,
|
||||
lines: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'NextUp',
|
||||
viewType: SectionsViewType.NextUp,
|
||||
type: 'nextup',
|
||||
view: SectionsView.NextUp,
|
||||
cardOptions: {
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
shape: 'overflowBackdrop',
|
||||
preferThumb: true,
|
||||
inheritThumb:
|
||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||
showParentTitle: true,
|
||||
overlayText: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderLatestMusic',
|
||||
viewType: SectionsViewType.LatestMedia,
|
||||
type: 'Audio',
|
||||
view: SectionsView.LatestMusic,
|
||||
parametersOptions: {
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderRecentlyPlayed',
|
||||
type: 'Audio',
|
||||
view: SectionsView.RecentlyPlayedMusic,
|
||||
parametersOptions: {
|
||||
sortBy: [ItemSortBy.DatePlayed],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
action: 'instantmix',
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'HeaderFrequentlyPlayed',
|
||||
type: 'Audio',
|
||||
view: SectionsView.FrequentlyPlayedMusic,
|
||||
parametersOptions: {
|
||||
sortBy: [ItemSortBy.PlayCount],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
includeItemTypes: [BaseItemKind.Audio]
|
||||
},
|
||||
cardOptions: {
|
||||
showUnplayedIndicator: false,
|
||||
shape: 'overflowSquare',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
action: 'instantmix',
|
||||
lazy: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
cardLayout: false,
|
||||
coverImage: true
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
interface SuggestionsItemsContainerProps {
|
||||
parentId: ParentId;
|
||||
sectionsView: SectionsView[];
|
||||
}
|
||||
|
||||
const SuggestionsItemsContainer: FC<SuggestionsItemsContainerProps> = ({
|
||||
parentId,
|
||||
sectionsView
|
||||
}) => {
|
||||
const suggestionsSections = getSuggestionsSections();
|
||||
|
||||
return (
|
||||
<>
|
||||
{suggestionsSections
|
||||
.filter((section) => sectionsView.includes(section.view))
|
||||
.map((section) => (
|
||||
<SuggestionsSectionContainer
|
||||
key={section.view}
|
||||
parentId={parentId}
|
||||
section={section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsItemsContainer;
|
|
@ -1,50 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useGetItemsBySectionType } from 'hooks/useFetchItems';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import SectionContainer from './SectionContainer';
|
||||
|
||||
import { Sections } from 'types/suggestionsSections';
|
||||
import { ParentId } from 'types/library';
|
||||
|
||||
interface SuggestionsSectionContainerProps {
|
||||
parentId: ParentId;
|
||||
section: Sections;
|
||||
}
|
||||
|
||||
const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
|
||||
parentId,
|
||||
section
|
||||
}) => {
|
||||
const getRouteUrl = () => {
|
||||
return appRouter.getRouteUrl('list', {
|
||||
serverId: window.ApiClient.serverId(),
|
||||
itemTypes: section.type,
|
||||
parentId: parentId
|
||||
});
|
||||
};
|
||||
|
||||
const { isLoading, data: items } = useGetItemsBySectionType(
|
||||
section,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
sectionTitle={globalize.translate(section.name)}
|
||||
items={items ?? []}
|
||||
url={getRouteUrl()}
|
||||
cardOptions={{
|
||||
...section.cardOptions
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsSectionContainer;
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
RecommendationDto,
|
||||
RecommendationType
|
||||
} from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
import {
|
||||
useGetMovieRecommendations,
|
||||
useGetSuggestionSectionsWithItems
|
||||
} from 'hooks/useFetchItems';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
import { Section, SectionType } from 'types/sections';
|
||||
|
||||
interface SuggestionsSectionViewProps {
|
||||
parentId: ParentId;
|
||||
sectionType: SectionType[];
|
||||
isMovieRecommendationEnabled: boolean | undefined;
|
||||
}
|
||||
|
||||
const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
|
||||
parentId,
|
||||
sectionType,
|
||||
isMovieRecommendationEnabled = false
|
||||
}) => {
|
||||
const { isLoading, data: sectionsWithItems } =
|
||||
useGetSuggestionSectionsWithItems(parentId, sectionType);
|
||||
|
||||
const {
|
||||
isLoading: isRecommendationsLoading,
|
||||
data: movieRecommendationsItems
|
||||
} = useGetMovieRecommendations(isMovieRecommendationEnabled, parentId);
|
||||
|
||||
if (isLoading || isRecommendationsLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) {
|
||||
return (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoItemsAvailable')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getRouteUrl = (section: Section) => {
|
||||
return appRouter.getRouteUrl('list', {
|
||||
serverId: window.ApiClient.serverId(),
|
||||
itemTypes: section.itemTypes,
|
||||
parentId: parentId
|
||||
});
|
||||
};
|
||||
|
||||
const getRecommendationTittle = (recommendation: RecommendationDto) => {
|
||||
let title = '';
|
||||
|
||||
switch (recommendation.RecommendationType) {
|
||||
case RecommendationType.SimilarToRecentlyPlayed:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouWatched',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.SimilarToLikedItem:
|
||||
title = globalize.translate(
|
||||
'RecommendationBecauseYouLike',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedDirector:
|
||||
title = globalize.translate(
|
||||
'RecommendationDirectedBy',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
|
||||
case RecommendationType.HasActorFromRecentlyPlayed:
|
||||
case RecommendationType.HasLikedActor:
|
||||
title = globalize.translate(
|
||||
'RecommendationStarring',
|
||||
recommendation.BaselineItemName
|
||||
);
|
||||
break;
|
||||
}
|
||||
return escapeHTML(title);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{sectionsWithItems?.map(({ section, items }) => (
|
||||
<SectionContainer
|
||||
key={section.type}
|
||||
sectionTitle={globalize.translate(section.name)}
|
||||
items={items ?? []}
|
||||
url={getRouteUrl(section)}
|
||||
cardOptions={{
|
||||
...section.cardOptions,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
overlayText: false
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{movieRecommendationsItems?.map((recommendation, index) => (
|
||||
<SectionContainer
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||
sectionTitle={getRecommendationTittle(recommendation)}
|
||||
items={recommendation.Items ?? []}
|
||||
cardOptions={{
|
||||
shape: 'overflowPortrait',
|
||||
showYear: true,
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsSectionView;
|
|
@ -1,33 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import SuggestionsItemsContainer from './SuggestionsItemsContainer';
|
||||
import RecommendationItemsContainer from './RecommendationItemsContainer';
|
||||
import { ParentId } from 'types/library';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
|
||||
interface SuggestionsViewProps {
|
||||
parentId: ParentId;
|
||||
suggestionSectionViews: SectionsView[] | undefined;
|
||||
isMovieRecommendations: boolean | undefined;
|
||||
}
|
||||
|
||||
const SuggestionsView: FC<SuggestionsViewProps> = ({
|
||||
parentId,
|
||||
suggestionSectionViews = [],
|
||||
isMovieRecommendations = false
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
<SuggestionsItemsContainer
|
||||
parentId={parentId}
|
||||
sectionsView={suggestionSectionViews}
|
||||
/>
|
||||
|
||||
{isMovieRecommendations && (
|
||||
<RecommendationItemsContainer parentId={parentId} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsView;
|
|
@ -321,7 +321,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FiltersGenres
|
||||
filters={data}
|
||||
genresOptions={data.Genres}
|
||||
libraryViewSettings={
|
||||
libraryViewSettings
|
||||
}
|
||||
|
@ -355,7 +355,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FiltersOfficialRatings
|
||||
filters={data}
|
||||
OfficialRatingsOptions={data.OfficialRatings}
|
||||
libraryViewSettings={
|
||||
libraryViewSettings
|
||||
}
|
||||
|
@ -382,7 +382,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FiltersTags
|
||||
filters={data}
|
||||
tagsOptions={data.Tags}
|
||||
libraryViewSettings={
|
||||
libraryViewSettings
|
||||
}
|
||||
|
@ -409,7 +409,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FiltersYears
|
||||
filters={data}
|
||||
yearsOptions={data.Years}
|
||||
libraryViewSettings={
|
||||
libraryViewSettings
|
||||
}
|
||||
|
@ -422,7 +422,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{isFiltersStudiosEnabled() && (
|
||||
{isFiltersStudiosEnabled() && studios && (
|
||||
<Accordion
|
||||
expanded={expanded === 'filtersStudios'}
|
||||
onChange={handleChange('filtersStudios')}
|
||||
|
@ -437,7 +437,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FiltersStudios
|
||||
filters={studios}
|
||||
studiosOptions={studios}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={
|
||||
setLibraryViewSettings
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
|||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface FiltersGenresProps {
|
||||
filters?: QueryFiltersLegacy;
|
||||
genresOptions: string[];
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
}
|
||||
|
||||
const FiltersGenres: FC<FiltersGenresProps> = ({
|
||||
filters,
|
||||
genresOptions,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
|
@ -40,7 +39,7 @@ const FiltersGenres: FC<FiltersGenresProps> = ({
|
|||
|
||||
return (
|
||||
<FormGroup>
|
||||
{filters?.Genres?.map((filter) => (
|
||||
{genresOptions.map((filter) => (
|
||||
<FormControlLabel
|
||||
key={filter}
|
||||
control={
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
|||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface FiltersOfficialRatingsProps {
|
||||
filters?: QueryFiltersLegacy;
|
||||
OfficialRatingsOptions: string[];
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
}
|
||||
|
||||
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
||||
filters,
|
||||
OfficialRatingsOptions,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
|
@ -40,7 +39,7 @@ const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
|||
|
||||
return (
|
||||
<FormGroup>
|
||||
{filters?.OfficialRatings?.map((filter) => (
|
||||
{OfficialRatingsOptions.map((filter) => (
|
||||
<FormControlLabel
|
||||
key={filter}
|
||||
control={
|
||||
|
|
|
@ -55,6 +55,7 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
|
|||
&& viewType !== LibraryTab.Artists
|
||||
&& viewType !== LibraryTab.AlbumArtists
|
||||
&& viewType !== LibraryTab.Songs
|
||||
&& viewType !== LibraryTab.Channels
|
||||
) {
|
||||
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
||||
visibleFiltersStatus.push(ItemFilter.IsPlayed);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
@ -6,13 +6,13 @@ import Checkbox from '@mui/material/Checkbox';
|
|||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface FiltersStudiosProps {
|
||||
filters?: BaseItemDtoQueryResult;
|
||||
studiosOptions: BaseItemDto[];
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
}
|
||||
|
||||
const FiltersStudios: FC<FiltersStudiosProps> = ({
|
||||
filters,
|
||||
studiosOptions,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
|
@ -40,7 +40,7 @@ const FiltersStudios: FC<FiltersStudiosProps> = ({
|
|||
|
||||
return (
|
||||
<FormGroup>
|
||||
{filters?.Items?.map((filter) => (
|
||||
{studiosOptions?.map((filter) => (
|
||||
<FormControlLabel
|
||||
key={filter.Id}
|
||||
control={
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
|||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface FiltersTagsProps {
|
||||
filters?: QueryFiltersLegacy;
|
||||
tagsOptions: string[];
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
}
|
||||
|
||||
const FiltersTags: FC<FiltersTagsProps> = ({
|
||||
filters,
|
||||
tagsOptions,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
|
@ -40,7 +39,7 @@ const FiltersTags: FC<FiltersTagsProps> = ({
|
|||
|
||||
return (
|
||||
<FormGroup>
|
||||
{filters?.Tags?.map((filter) => (
|
||||
{tagsOptions.map((filter) => (
|
||||
<FormControlLabel
|
||||
key={filter}
|
||||
control={
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
|||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface FiltersYearsProps {
|
||||
filters?: QueryFiltersLegacy;
|
||||
yearsOptions: number[];
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
}
|
||||
|
||||
const FiltersYears: FC<FiltersYearsProps> = ({
|
||||
filters,
|
||||
yearsOptions,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
|
@ -40,7 +39,7 @@ const FiltersYears: FC<FiltersYearsProps> = ({
|
|||
|
||||
return (
|
||||
<FormGroup>
|
||||
{filters?.Years?.map((filter) => (
|
||||
{yearsOptions.map((filter) => (
|
||||
<FormControlLabel
|
||||
key={filter}
|
||||
control={
|
||||
|
|
|
@ -4,9 +4,10 @@ import Tabs from '@mui/material/Tabs';
|
|||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { debounce } from 'lodash-es';
|
||||
import React, { FC, useCallback, useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
|
||||
import TabRoutes from './tabRoutes';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
|
||||
interface AppTabsParams {
|
||||
isDrawerOpen: boolean
|
||||
|
@ -18,14 +19,7 @@ const AppTabs: FC<AppTabsParams> = ({
|
|||
isDrawerOpen
|
||||
}) => {
|
||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
const location = useLocation();
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const libraryId = location.pathname === '/livetv.html' ?
|
||||
'livetv' : searchParams.get('topParentId');
|
||||
const activeTab = searchParamsTab !== null ?
|
||||
parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, libraryId);
|
||||
const { searchParams, setSearchParams, activeTab } = useCurrentTab();
|
||||
|
||||
// HACK: Force resizing to workaround upstream bug with tab resizing
|
||||
// https://github.com/mui/material-ui/issues/24011
|
||||
|
@ -71,7 +65,7 @@ const AppTabs: FC<AppTabsParams> = ({
|
|||
{
|
||||
route.tabs.map(({ index, label }) => (
|
||||
<Tab
|
||||
key={`${route}-tab-${index}`}
|
||||
key={`${route.path}-tab-${index}`}
|
||||
label={label}
|
||||
data-tab-index={`${index}`}
|
||||
onClick={onTabClick}
|
||||
|
|
|
@ -68,7 +68,7 @@ const TabRoutes: TabRoute[] = [
|
|||
{
|
||||
index: 5,
|
||||
label: globalize.translate('Series'),
|
||||
value: LibraryTab.Series
|
||||
value: LibraryTab.SeriesTimers
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -155,7 +155,7 @@ const TabRoutes: TabRoute[] = [
|
|||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Shows'),
|
||||
value: LibraryTab.Shows,
|
||||
value: LibraryTab.Series,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
|
|
|
@ -7,5 +7,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
|||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
||||
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
|
||||
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental }
|
||||
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
|
||||
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental }
|
||||
];
|
||||
|
|
|
@ -13,12 +13,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'list',
|
||||
view: 'list.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetv.html',
|
||||
pageProps: {
|
||||
controller: 'livetv/livetvsuggested',
|
||||
view: 'livetv.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesmenu.html',
|
||||
pageProps: {
|
||||
|
|
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 { CollectionType } from 'types/collectionType';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
import { MovieSuggestionsSectionsView } from 'types/sections';
|
||||
|
||||
const moviesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Movies,
|
||||
|
@ -40,13 +40,7 @@ const trailersTabContent: LibraryTabContent = {
|
|||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.Movies,
|
||||
sectionsType: {
|
||||
suggestionSectionsView: [
|
||||
SectionsView.ContinueWatchingMovies,
|
||||
SectionsView.LatestMovies
|
||||
],
|
||||
isMovieRecommendations: true
|
||||
}
|
||||
sectionsView: MovieSuggestionsSectionsView
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
|
@ -65,8 +59,8 @@ const moviesTabMapping: LibraryTabMapping = {
|
|||
};
|
||||
|
||||
const Movies: FC = () => {
|
||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||
const currentTab = moviesTabMapping[currentTabIndex];
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = moviesTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
@ -75,9 +69,9 @@ const Movies: FC = () => {
|
|||
backDropType='movie'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={searchParamsParentId}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import PageTabContent from '../../components/library/PageTabContent';
|
|||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
import { MusicSuggestionsSectionsView } from 'types/sections';
|
||||
|
||||
const albumArtistsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.AlbumArtists,
|
||||
|
@ -47,13 +47,7 @@ const songsTabContent: LibraryTabContent = {
|
|||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.Music,
|
||||
sectionsType: {
|
||||
suggestionSectionsView: [
|
||||
SectionsView.LatestMusic,
|
||||
SectionsView.FrequentlyPlayedMusic,
|
||||
SectionsView.RecentlyPlayedMusic
|
||||
]
|
||||
}
|
||||
sectionsView: MusicSuggestionsSectionsView
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
|
@ -73,8 +67,8 @@ const musicTabMapping: LibraryTabMapping = {
|
|||
};
|
||||
|
||||
const Music: FC = () => {
|
||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||
const currentTab = musicTabMapping[currentTabIndex];
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = musicTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
@ -83,9 +77,9 @@ const Music: FC = () => {
|
|||
backDropType='musicartist'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={searchParamsParentId}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -5,8 +5,8 @@ import Page from 'components/Page';
|
|||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { TvShowSuggestionsSectionsView } from 'types/sections';
|
||||
|
||||
const episodesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Episodes,
|
||||
|
@ -39,13 +39,7 @@ const upcomingTabContent: LibraryTabContent = {
|
|||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.TvShows,
|
||||
sectionsType: {
|
||||
suggestionSectionsView: [
|
||||
SectionsView.ContinueWatchingEpisode,
|
||||
SectionsView.LatestEpisode,
|
||||
SectionsView.NextUp
|
||||
]
|
||||
}
|
||||
sectionsView: TvShowSuggestionsSectionsView
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
|
@ -64,8 +58,8 @@ const tvShowsTabMapping: LibraryTabMapping = {
|
|||
};
|
||||
|
||||
const Shows: FC = () => {
|
||||
const { searchParamsParentId, currentTabIndex } = useCurrentTab();
|
||||
const currentTab = tvShowsTabMapping[currentTabIndex];
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = tvShowsTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
@ -74,9 +68,9 @@ const Shows: FC = () => {
|
|||
backDropType='series'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${searchParamsParentId}`}
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={searchParamsParentId}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -75,7 +75,7 @@ function getLandingScreenOptions(type) {
|
|||
} else if (type === 'tvshows') {
|
||||
list.push({
|
||||
name: globalize.translate('Shows'),
|
||||
value: LibraryTab.Shows,
|
||||
value: LibraryTab.Series,
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
|
@ -152,7 +152,7 @@ function getLandingScreenOptions(type) {
|
|||
});
|
||||
list.push({
|
||||
name: globalize.translate('Series'),
|
||||
value: LibraryTab.Series
|
||||
value: LibraryTab.SeriesTimers
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -545,6 +545,30 @@ class AppRouter {
|
|||
urlForList += '&IsFavorite=true';
|
||||
}
|
||||
|
||||
if (options.isAiring) {
|
||||
urlForList += '&IsAiring=true';
|
||||
}
|
||||
|
||||
if (options.isMovie) {
|
||||
urlForList += '&IsMovie=true';
|
||||
}
|
||||
|
||||
if (options.isSeries) {
|
||||
urlForList += '&IsSeries=true&IsMovie=false&IsNews=false';
|
||||
}
|
||||
|
||||
if (options.isSports) {
|
||||
urlForList += '&IsSports=true';
|
||||
}
|
||||
|
||||
if (options.isKids) {
|
||||
urlForList += '&IsKids=true';
|
||||
}
|
||||
|
||||
if (options.isNews) {
|
||||
urlForList += '&IsNews=true';
|
||||
}
|
||||
|
||||
return urlForList;
|
||||
}
|
||||
|
||||
|
|
|
@ -199,7 +199,7 @@ function getDefaultTabIndex(folderId) {
|
|||
return 3;
|
||||
case LibraryTab.Schedule:
|
||||
return 4;
|
||||
case LibraryTab.Series:
|
||||
case LibraryTab.SeriesTimers:
|
||||
return 5;
|
||||
default:
|
||||
return 0;
|
||||
|
|
|
@ -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 loading from 'components/loading/loading';
|
||||
import focusManager from 'components/focusManager';
|
||||
import { LibraryViewSettings, ParentId, ViewMode } from 'types/library';
|
||||
import { ParentId } from 'types/library';
|
||||
|
||||
function disableEvent(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
@ -37,20 +37,18 @@ function getShortcutOptions() {
|
|||
|
||||
interface ItemsContainerProps {
|
||||
className?: string;
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
isContextMenuEnabled?: boolean;
|
||||
isMultiSelectEnabled?: boolean;
|
||||
isDragreOrderEnabled?: boolean;
|
||||
dataMonitor?: string;
|
||||
parentId?: ParentId;
|
||||
reloadItems: () => void;
|
||||
reloadItems?: () => void;
|
||||
getItemsHtml?: () => string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ItemsContainer: FC<ItemsContainerProps> = ({
|
||||
className,
|
||||
libraryViewSettings,
|
||||
isContextMenuEnabled,
|
||||
isMultiSelectEnabled,
|
||||
isDragreOrderEnabled,
|
||||
|
@ -146,7 +144,9 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
|||
});
|
||||
loading.hide();
|
||||
} catch (error) {
|
||||
console.error('[Drag-Drop] error playlists Move Item: ' + error);
|
||||
loading.hide();
|
||||
if (!reloadItems) return;
|
||||
reloadItems();
|
||||
}
|
||||
},
|
||||
|
@ -174,6 +174,7 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
|||
|
||||
const notifyRefreshNeeded = useCallback(
|
||||
(isInForeground: boolean) => {
|
||||
if (!reloadItems) return;
|
||||
if (isInForeground === true) {
|
||||
reloadItems();
|
||||
} else {
|
||||
|
@ -506,9 +507,6 @@ const ItemsContainer: FC<ItemsContainerProps> = ({
|
|||
const itemsContainerClass = classNames(
|
||||
'itemsContainer',
|
||||
{ 'itemsContainer-tv': layoutManager.tv },
|
||||
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
||||
'vertical-list' :
|
||||
'vertical-wrap',
|
||||
className
|
||||
);
|
||||
|
||||
|
|
|
@ -3,17 +3,22 @@ import { useLocation, useSearchParams } from 'react-router-dom';
|
|||
|
||||
const useCurrentTab = () => {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const searchParamsParentId = searchParams.get('topParentId');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const currentTabIndex: number =
|
||||
searchParamsTab !== null ?
|
||||
parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
||||
const libraryId =
|
||||
location.pathname === '/livetv.html' ?
|
||||
'livetv' :
|
||||
searchParams.get('topParentId');
|
||||
const activeTab: number =
|
||||
searchParamsTab !== null ?
|
||||
parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, libraryId);
|
||||
|
||||
return {
|
||||
searchParamsParentId,
|
||||
currentTabIndex
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
libraryId,
|
||||
activeTab
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, ItemsApiGetItemsRequest, PlaylistsApiMoveItemRequest, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||
|
@ -15,6 +15,7 @@ import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
|
|||
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import datetime from 'scripts/datetime';
|
||||
|
@ -22,9 +23,10 @@ import globalize from 'scripts/globalize';
|
|||
|
||||
import { JellyfinApiContext, useApi } from './useApi';
|
||||
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
||||
import { Sections, SectionsViewType } from 'types/suggestionsSections';
|
||||
import { getProgramSections, getSuggestionSections } from 'utils/sections';
|
||||
import { LibraryViewSettings, ParentId } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { Section, SectionApiMethod, SectionType } from 'types/sections';
|
||||
|
||||
const fetchGetItem = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
|
@ -48,10 +50,11 @@ const fetchGetItem = async (
|
|||
|
||||
export const useGetItem = (parentId: ParentId) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['Item', parentId],
|
||||
queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -116,142 +119,12 @@ const fetchGetMovieRecommendations = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const useGetMovieRecommendations = (parentId: ParentId) => {
|
||||
export const useGetMovieRecommendations = (isMovieRecommendationEnabled: boolean, parentId: ParentId) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['MovieRecommendations', parentId],
|
||||
queryKey: ['MovieRecommendations', isMovieRecommendationEnabled, parentId],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetMovieRecommendations(currentApi, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetItemsBySuggestionsType = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
sections: Sections,
|
||||
parentId: ParentId,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api, user } = currentApi;
|
||||
if (api && user?.Id) {
|
||||
let response;
|
||||
switch (sections.viewType) {
|
||||
case SectionsViewType.NextUp: {
|
||||
response = (
|
||||
await getTvShowsApi(api).getNextUp(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 25,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionsViewType.ResumeItems: {
|
||||
response = (
|
||||
await getItemsApi(api).getResumeItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Thumb],
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionsViewType.LatestMedia: {
|
||||
response = (
|
||||
await getUserLibraryApi(api).getLatestMedia(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ ImageType.Primary, ImageType.Thumb ],
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
response = (
|
||||
await getItemsApi(api).getItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
recursive: true,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||
filters: [ItemFilter.IsPlayed],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
limit: 25,
|
||||
enableTotalRecordCount: false,
|
||||
...sections.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetItemsBySectionType = (
|
||||
sections: Sections,
|
||||
parentId: ParentId
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ItemsBySuggestionsType', sections.view],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetItemsBySuggestionsType(
|
||||
currentApi,
|
||||
sections,
|
||||
parentId,
|
||||
{ signal }
|
||||
),
|
||||
enabled: !!sections.view
|
||||
isMovieRecommendationEnabled ? fetchGetMovieRecommendations(currentApi, parentId, { signal }) : []
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -314,17 +187,18 @@ const fetchGetStudios = async (
|
|||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
return response.data.Items;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['Studios', parentId, itemType],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetStudios(currentApi, parentId, itemType, { signal }),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -355,13 +229,14 @@ export const useGetQueryFiltersLegacy = (
|
|||
itemType: BaseItemKind[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const isLivetv = parentId === 'livetv';
|
||||
return useQuery({
|
||||
queryKey: ['QueryFiltersLegacy', parentId, itemType],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, {
|
||||
signal
|
||||
}),
|
||||
enabled: !!parentId
|
||||
enabled: !!parentId && !isLivetv
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -434,6 +309,34 @@ const fetchGetItemsViewByType = async (
|
|||
}
|
||||
);
|
||||
break;
|
||||
case LibraryTab.Channels: {
|
||||
response = await getLiveTvApi(api).getLiveTvChannels(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||
startIndex: libraryViewSettings.StartIndex,
|
||||
isFavorite: libraryViewSettings.Filters?.Status?.includes(ItemFilter.IsFavorite) ?
|
||||
true :
|
||||
undefined,
|
||||
enableImageTypes: [ImageType.Primary]
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case LibraryTab.SeriesTimers:
|
||||
response = await getLiveTvApi(api).getSeriesTimers(
|
||||
{
|
||||
sortBy: 'SortName',
|
||||
sortOrder: SortOrder.Ascending
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
break;
|
||||
default: {
|
||||
response = await getItemsApi(api).getItems(
|
||||
{
|
||||
|
@ -505,8 +408,10 @@ export const useGetItemsViewByType = (
|
|||
LibraryTab.Songs,
|
||||
LibraryTab.Books,
|
||||
LibraryTab.Photos,
|
||||
LibraryTab.Videos
|
||||
].includes(viewType) && !!parentId
|
||||
LibraryTab.Videos,
|
||||
LibraryTab.Channels,
|
||||
LibraryTab.SeriesTimers
|
||||
].includes(viewType)
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -690,3 +595,336 @@ export const useTogglePlayedMutation = () => {
|
|||
fetchUpdatePlayedState(currentApi, itemId, playedState )
|
||||
});
|
||||
};
|
||||
|
||||
export type GroupsTimers = {
|
||||
name: string;
|
||||
timerInfo: TimerInfoDto[];
|
||||
};
|
||||
|
||||
function groupsTimers(timers: TimerInfoDto[], indexByDate?: boolean) {
|
||||
const items = timers.map(function (t) {
|
||||
t.Type = 'Timer';
|
||||
return t;
|
||||
});
|
||||
const groups: GroupsTimers[] = [];
|
||||
let currentGroupName = '';
|
||||
let currentGroup: TimerInfoDto[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
let dateText = '';
|
||||
|
||||
if (indexByDate !== false && item.StartDate) {
|
||||
try {
|
||||
const premiereDate = datetime.parseISO8601Date(item.StartDate, true);
|
||||
dateText = datetime.toLocaleDateString(premiereDate, {
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('error parsing premiereDate:' + item.StartDate + '; error: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
if (dateText != currentGroupName) {
|
||||
if (currentGroup.length) {
|
||||
groups.push({
|
||||
name: currentGroupName,
|
||||
timerInfo: currentGroup
|
||||
});
|
||||
}
|
||||
|
||||
currentGroupName = dateText;
|
||||
currentGroup = [item];
|
||||
} else {
|
||||
currentGroup.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGroup.length) {
|
||||
groups.push({
|
||||
name: currentGroupName,
|
||||
timerInfo: currentGroup
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
const fetchGetTimers = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
indexByDate?: boolean,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api } = currentApi;
|
||||
if (api) {
|
||||
const response = await getLiveTvApi(api).getTimers(
|
||||
{
|
||||
isActive: false,
|
||||
isScheduled: true
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
);
|
||||
|
||||
const timers = response.data.Items ?? [];
|
||||
|
||||
return groupsTimers(timers, indexByDate);
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetTimers = (isUpcomingRecordingsEnabled: boolean, indexByDate?: boolean) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['Timers', isUpcomingRecordingsEnabled, indexByDate],
|
||||
queryFn: ({ signal }) =>
|
||||
isUpcomingRecordingsEnabled ? fetchGetTimers(currentApi, indexByDate, { signal }) : []
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetSectionItems = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId: ParentId,
|
||||
section: Section,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const { api, user } = currentApi;
|
||||
if (api && user?.Id) {
|
||||
let response;
|
||||
switch (section.apiMethod) {
|
||||
case SectionApiMethod.RecommendedPrograms: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecommendedPrograms(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 12,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.LiveTvPrograms: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getLiveTvPrograms(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 12,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.PrimaryImageAspectRatio
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.Recordings: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecordings(
|
||||
{
|
||||
userId: user.Id,
|
||||
enableImageTypes: [ImageType.Primary, ImageType.Thumb, ImageType.Backdrop],
|
||||
enableTotalRecordCount: false,
|
||||
fields: [
|
||||
ItemFields.CanDelete,
|
||||
ItemFields.PrimaryImageAspectRatio
|
||||
],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.RecordingFolders: {
|
||||
response = (
|
||||
await getLiveTvApi(api).getRecordingFolders(
|
||||
{
|
||||
userId: user.Id
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.NextUp: {
|
||||
response = (
|
||||
await getTvShowsApi(api).getNextUp(
|
||||
{
|
||||
userId: user.Id,
|
||||
limit: 25,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop,
|
||||
ImageType.Thumb
|
||||
],
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.ResumeItems: {
|
||||
response = (
|
||||
await getItemsApi(api).getResumeItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Thumb],
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
case SectionApiMethod.LatestMedia: {
|
||||
response = (
|
||||
await getUserLibraryApi(api).getLatestMedia(
|
||||
{
|
||||
userId: user.Id,
|
||||
fields: [
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.MediaSourceCount
|
||||
],
|
||||
parentId: parentId ?? undefined,
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [ImageType.Primary],
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
response = (
|
||||
await getItemsApi(api).getItems(
|
||||
{
|
||||
userId: user.Id,
|
||||
parentId: parentId ?? undefined,
|
||||
recursive: true,
|
||||
limit: 25,
|
||||
enableTotalRecordCount: false,
|
||||
...section.parametersOptions
|
||||
},
|
||||
{
|
||||
signal: options?.signal
|
||||
}
|
||||
)
|
||||
).data.Items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
type SectionWithItems = {
|
||||
section: Section;
|
||||
items: BaseItemDto[];
|
||||
};
|
||||
|
||||
const getSectionsWithItems = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId: ParentId,
|
||||
sections: Section[],
|
||||
sectionType?: SectionType[],
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (sectionType) {
|
||||
sections = sections.filter((section) => sectionType.includes(section.type));
|
||||
}
|
||||
|
||||
const updatedSectionWithItems: SectionWithItems[] = [];
|
||||
|
||||
for (const section of sections) {
|
||||
try {
|
||||
const items = await fetchGetSectionItems(
|
||||
currentApi, parentId, section, options
|
||||
);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
updatedSectionWithItems.push({
|
||||
section,
|
||||
items
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error occurred for section ${section.type}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedSectionWithItems;
|
||||
};
|
||||
|
||||
export const useGetSuggestionSectionsWithItems = (
|
||||
parentId: ParentId,
|
||||
suggestionSectionType: SectionType[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const sections = getSuggestionSections();
|
||||
return useQuery({
|
||||
queryKey: ['SuggestionSectionWithItems', suggestionSectionType],
|
||||
queryFn: ({ signal }) =>
|
||||
getSectionsWithItems(currentApi, parentId, sections, suggestionSectionType, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProgramsSectionsWithItems = (
|
||||
parentId: ParentId,
|
||||
programSectionType: SectionType[]
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
const sections = getProgramSections();
|
||||
return useQuery({
|
||||
queryKey: ['ProgramSectionWithItems', programSectionType],
|
||||
queryFn: ({ signal }) =>
|
||||
getSectionsWithItems(currentApi, parentId, sections, programSectionType, { signal })
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -323,6 +323,7 @@
|
|||
"HeaderAdmin": "Administration",
|
||||
"HeaderAlbumArtists": "Album artists",
|
||||
"HeaderAlert": "Alert",
|
||||
"HeaderAllRecordings": "All Recordings",
|
||||
"HeaderAllowMediaDeletionFrom": "Allow media deletion from",
|
||||
"HeaderApiKey": "API Key",
|
||||
"HeaderApiKeys": "API Keys",
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface CardOptions {
|
|||
overlayMoreButton?: boolean;
|
||||
overlayPlayButton?: boolean;
|
||||
overlayText?: boolean;
|
||||
preferThumb?: boolean;
|
||||
preferThumb?: boolean | string | null;
|
||||
preferDisc?: boolean;
|
||||
preferLogo?: boolean;
|
||||
scalable?: boolean;
|
||||
|
|
|
@ -15,7 +15,7 @@ export enum LibraryTab {
|
|||
Recordings = 'recordings',
|
||||
Schedule = 'schedule',
|
||||
Series = 'series',
|
||||
Shows = 'shows',
|
||||
SeriesTimers = 'seriestimers',
|
||||
Songs = 'songs',
|
||||
Suggestions = 'suggestions',
|
||||
Trailers = 'trailers',
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { LibraryTab } from './libraryTab';
|
||||
import { CollectionType } from './collectionType';
|
||||
import { SectionsView } from './suggestionsSections';
|
||||
import { SectionType } from './sections';
|
||||
|
||||
export interface SuggestionsSectionsType {
|
||||
suggestionSectionsView: SectionsView[];
|
||||
export interface SectionsView {
|
||||
suggestionSections?: SectionType[];
|
||||
favoriteSections?: SectionType[];
|
||||
programSections?: SectionType[];
|
||||
isMovieRecommendations?: boolean;
|
||||
isLiveTvUpcomingRecordings?: boolean;
|
||||
}
|
||||
|
||||
export interface LibraryTabContent {
|
||||
viewType: LibraryTab;
|
||||
itemType?: BaseItemKind[];
|
||||
collectionType?: CollectionType;
|
||||
sectionsType?: SuggestionsSectionsType;
|
||||
sectionsView?: SectionsView;
|
||||
isPaginationEnabled?: boolean;
|
||||
isBtnPlayAllEnabled?: boolean;
|
||||
isBtnQueueEnabled?: boolean;
|
||||
isBtnShuffleEnabled?: boolean;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import { CollectionType } from './collectionType';
|
||||
|
||||
export interface ListOptions {
|
||||
items?: BaseItemDto[] | null;
|
||||
items?: BaseItemDto[] | SeriesTimerInfoDto[] | null;
|
||||
index?: string;
|
||||
showIndex?: boolean;
|
||||
action?: string | null;
|
||||
|
|
109
src/types/sections.ts
Normal file
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