mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #5166 from grafixeyehero/Convert-list&Card-view-to-react
This commit is contained in:
commit
f0be743503
87 changed files with 5070 additions and 348 deletions
25
src/components/cardbuilder/Card/Card.tsx
Normal file
25
src/components/cardbuilder/Card/Card.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { type FC } from 'react';
|
||||
import useCard from './useCard';
|
||||
import CardWrapper from './CardWrapper';
|
||||
import CardBox from './CardBox';
|
||||
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
|
||||
interface CardProps {
|
||||
item?: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({ item = {}, cardOptions }) => {
|
||||
const { getCardWrapperProps, getCardBoxProps } = useCard({ item, cardOptions } );
|
||||
const cardWrapperProps = getCardWrapperProps();
|
||||
const cardBoxProps = getCardBoxProps();
|
||||
return (
|
||||
<CardWrapper {...cardWrapperProps}>
|
||||
<CardBox {...cardBoxProps} />
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
78
src/components/cardbuilder/Card/CardBox.tsx
Normal file
78
src/components/cardbuilder/Card/CardBox.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React, { type FC } from 'react';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
|
||||
import CardOverlayButtons from './CardOverlayButtons';
|
||||
import CardHoverMenu from './CardHoverMenu';
|
||||
import CardOuterFooter from './CardOuterFooter';
|
||||
import CardContent from './CardContent';
|
||||
import { CardShape } from 'utils/card';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardBoxProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
className: string;
|
||||
shape: CardShape | undefined;
|
||||
imgUrl: string | undefined;
|
||||
blurhash: string | undefined;
|
||||
forceName: boolean;
|
||||
coveredImage: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
}
|
||||
|
||||
const CardBox: FC<CardBoxProps> = ({
|
||||
item,
|
||||
cardOptions,
|
||||
className,
|
||||
shape,
|
||||
imgUrl,
|
||||
blurhash,
|
||||
forceName,
|
||||
coveredImage,
|
||||
overlayText
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='cardScalable'>
|
||||
<div className={`cardPadder cardPadder-${shape}`}></div>
|
||||
<CardContent
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
coveredImage={coveredImage}
|
||||
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
blurhash={blurhash}
|
||||
forceName={forceName}
|
||||
/>
|
||||
{layoutManager.mobile && (
|
||||
<CardOverlayButtons
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{layoutManager.desktop
|
||||
&& !cardOptions.disableHoverMenu && (
|
||||
<CardHoverMenu
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!overlayText && (
|
||||
<CardOuterFooter
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
forceName={forceName}
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardBox;
|
||||
|
50
src/components/cardbuilder/Card/CardContent.tsx
Normal file
50
src/components/cardbuilder/Card/CardContent.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { getDefaultBackgroundClass } from '../cardBuilderUtils';
|
||||
import CardImageContainer from './CardImageContainer';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardContentProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
coveredImage: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
imgUrl: string | undefined;
|
||||
blurhash: string | undefined;
|
||||
forceName: boolean;
|
||||
}
|
||||
|
||||
const CardContent: FC<CardContentProps> = ({
|
||||
item,
|
||||
cardOptions,
|
||||
coveredImage,
|
||||
overlayText,
|
||||
imgUrl,
|
||||
blurhash,
|
||||
forceName
|
||||
}) => {
|
||||
const cardContentClass = classNames(
|
||||
'cardContent',
|
||||
{ [getDefaultBackgroundClass(item.Name)]: !imgUrl }
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cardContentClass}
|
||||
>
|
||||
<CardImageContainer
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
coveredImage={coveredImage}
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
blurhash={blurhash}
|
||||
forceName={forceName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardContent;
|
81
src/components/cardbuilder/Card/CardFooterText.tsx
Normal file
81
src/components/cardbuilder/Card/CardFooterText.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import useCardText from './useCardText';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
const shouldShowDetailsMenu = (
|
||||
cardOptions: CardOptions,
|
||||
isOuterFooter: boolean
|
||||
) => {
|
||||
return (
|
||||
cardOptions.showDetailsMenu
|
||||
&& isOuterFooter
|
||||
&& cardOptions.cardLayout
|
||||
&& layoutManager.mobile
|
||||
&& cardOptions.cardFooterAside !== 'none'
|
||||
);
|
||||
};
|
||||
|
||||
interface LogoComponentProps {
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
const LogoComponent: FC<LogoComponentProps> = ({ logoUrl }) => {
|
||||
return <Box className='lazy cardFooterLogo' data-src={logoUrl} />;
|
||||
};
|
||||
|
||||
interface CardFooterTextProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
forceName: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
imgUrl: string | undefined;
|
||||
footerClass: string | undefined;
|
||||
progressBar?: React.JSX.Element | null;
|
||||
logoUrl?: string;
|
||||
isOuterFooter: boolean;
|
||||
}
|
||||
|
||||
const CardFooterText: FC<CardFooterTextProps> = ({
|
||||
item,
|
||||
cardOptions,
|
||||
forceName,
|
||||
imgUrl,
|
||||
footerClass,
|
||||
overlayText,
|
||||
progressBar,
|
||||
logoUrl,
|
||||
isOuterFooter
|
||||
}) => {
|
||||
const { cardTextLines } = useCardText({
|
||||
item,
|
||||
cardOptions,
|
||||
forceName,
|
||||
imgUrl,
|
||||
overlayText,
|
||||
isOuterFooter,
|
||||
cssClass: cardOptions.centerText ?
|
||||
'cardText cardTextCentered' :
|
||||
'cardText',
|
||||
forceLines: !cardOptions.overlayText,
|
||||
maxLines: cardOptions.lines
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className={footerClass}>
|
||||
{logoUrl && <LogoComponent logoUrl={logoUrl} />}
|
||||
{shouldShowDetailsMenu(cardOptions, isOuterFooter) && (
|
||||
<MoreVertIconButton className='itemAction btnCardOptions' />
|
||||
)}
|
||||
|
||||
{cardTextLines}
|
||||
|
||||
{progressBar}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardFooterText;
|
82
src/components/cardbuilder/Card/CardHoverMenu.tsx
Normal file
82
src/components/cardbuilder/Card/CardHoverMenu.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import classNames from 'classnames';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
|
||||
import PlayedButton from 'elements/emby-playstatebutton/PlayedButton';
|
||||
import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton';
|
||||
import PlayArrowIconButton from '../../common/PlayArrowIconButton';
|
||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardHoverMenuProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
const CardHoverMenu: FC<CardHoverMenuProps> = ({
|
||||
item,
|
||||
cardOptions
|
||||
}) => {
|
||||
const url = appRouter.getRouteUrl(item, {
|
||||
parentId: cardOptions.parentId
|
||||
});
|
||||
const btnCssClass =
|
||||
'paper-icon-button-light cardOverlayButton cardOverlayButton-hover itemAction';
|
||||
|
||||
const centerPlayButtonClass = classNames(
|
||||
btnCssClass,
|
||||
'cardOverlayFab-primary'
|
||||
);
|
||||
const { IsFavorite, Played } = item.UserData ?? {};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className='cardOverlayContainer'
|
||||
>
|
||||
<a
|
||||
href={url}
|
||||
aria-label={item.Name || ''}
|
||||
className='cardImageContainer'
|
||||
></a>
|
||||
|
||||
{playbackManager.canPlay(item) && (
|
||||
<PlayArrowIconButton
|
||||
className={centerPlayButtonClass}
|
||||
action='play'
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
|
||||
<ButtonGroup className='cardOverlayButton-br flex'>
|
||||
{itemHelper.canMarkPlayed(item) && cardOptions.enablePlayedButton !== false && (
|
||||
<PlayedButton
|
||||
className={btnCssClass}
|
||||
isPlayed={Played}
|
||||
itemId={item.Id}
|
||||
itemType={item.Type}
|
||||
queryKey={cardOptions.queryKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
{itemHelper.canRate(item) && cardOptions.enableRatingButton !== false && (
|
||||
<FavoriteButton
|
||||
className={btnCssClass}
|
||||
isFavorite={IsFavorite}
|
||||
itemId={item.Id}
|
||||
queryKey={cardOptions.queryKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MoreVertIconButton className={btnCssClass} />
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardHoverMenu;
|
83
src/components/cardbuilder/Card/CardImageContainer.tsx
Normal file
83
src/components/cardbuilder/Card/CardImageContainer.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import classNames from 'classnames';
|
||||
import useIndicator from 'components/indicators/useIndicator';
|
||||
import RefreshIndicator from 'elements/emby-itemrefreshindicator/RefreshIndicator';
|
||||
import Media from '../../common/Media';
|
||||
import CardInnerFooter from './CardInnerFooter';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardImageContainerProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
coveredImage: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
imgUrl: string | undefined;
|
||||
blurhash: string | undefined;
|
||||
forceName: boolean;
|
||||
}
|
||||
|
||||
const CardImageContainer: FC<CardImageContainerProps> = ({
|
||||
item,
|
||||
cardOptions,
|
||||
coveredImage,
|
||||
overlayText,
|
||||
imgUrl,
|
||||
blurhash,
|
||||
forceName
|
||||
}) => {
|
||||
const indicator = useIndicator(item);
|
||||
const cardImageClass = classNames(
|
||||
'cardImageContainer',
|
||||
{ coveredImage: coveredImage },
|
||||
{ 'coveredImage-contain': coveredImage && item.Type === BaseItemKind.TvChannel }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cardImageClass}>
|
||||
{cardOptions.disableIndicators !== true && (
|
||||
<Box className='indicators'>
|
||||
{indicator.getMediaSourceIndicator()}
|
||||
|
||||
<Box className='cardIndicators'>
|
||||
{cardOptions.missingIndicator !== false
|
||||
&& indicator.getMissingIndicator()}
|
||||
|
||||
{indicator.getTimerIndicator()}
|
||||
{indicator.getTypeIndicator()}
|
||||
|
||||
{cardOptions.showGroupCount ?
|
||||
indicator.getChildCountIndicator() :
|
||||
indicator.getPlayedIndicator()}
|
||||
|
||||
{(item.Type === BaseItemKind.CollectionFolder
|
||||
|| item.CollectionType)
|
||||
&& item.RefreshProgress && (
|
||||
<RefreshIndicator item={item} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Media item={item} imgUrl={imgUrl} blurhash={blurhash} imageType={cardOptions.imageType} />
|
||||
|
||||
{overlayText && (
|
||||
<CardInnerFooter
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
forceName={forceName}
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
progressBar={indicator.getProgressBar()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!overlayText && indicator.getProgressBar()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardImageContainer;
|
42
src/components/cardbuilder/Card/CardInnerFooter.tsx
Normal file
42
src/components/cardbuilder/Card/CardInnerFooter.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import CardFooterText from './CardFooterText';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardInnerFooterProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
imgUrl: string | undefined;
|
||||
progressBar?: React.JSX.Element | null;
|
||||
forceName: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
}
|
||||
|
||||
const CardInnerFooter: FC<CardInnerFooterProps> = ({
|
||||
item,
|
||||
cardOptions,
|
||||
imgUrl,
|
||||
overlayText,
|
||||
progressBar,
|
||||
forceName
|
||||
}) => {
|
||||
const footerClass = classNames('innerCardFooter', {
|
||||
fullInnerCardFooter: progressBar
|
||||
});
|
||||
|
||||
return (
|
||||
<CardFooterText
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
forceName={forceName}
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
footerClass={footerClass}
|
||||
progressBar={progressBar}
|
||||
isOuterFooter={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardInnerFooter;
|
45
src/components/cardbuilder/Card/CardOuterFooter.tsx
Normal file
45
src/components/cardbuilder/Card/CardOuterFooter.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getCardLogoUrl } from './cardHelper';
|
||||
import CardFooterText from './CardFooterText';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardOuterFooterProps {
|
||||
item: ItemDto
|
||||
cardOptions: CardOptions;
|
||||
imgUrl: string | undefined;
|
||||
forceName: boolean;
|
||||
overlayText: boolean | undefined
|
||||
}
|
||||
|
||||
const CardOuterFooter: FC<CardOuterFooterProps> = ({ item, cardOptions, overlayText, imgUrl, forceName }) => {
|
||||
const { api } = useApi();
|
||||
const logoInfo = getCardLogoUrl(item, api, cardOptions);
|
||||
const logoUrl = logoInfo.logoUrl;
|
||||
|
||||
const footerClass = classNames(
|
||||
'cardFooter',
|
||||
{ 'cardFooter-transparent': cardOptions.cardLayout },
|
||||
{ 'cardFooter-withlogo': logoUrl }
|
||||
);
|
||||
|
||||
return (
|
||||
<CardFooterText
|
||||
item={item}
|
||||
cardOptions={cardOptions}
|
||||
forceName={forceName}
|
||||
overlayText={overlayText}
|
||||
imgUrl={imgUrl}
|
||||
footerClass={footerClass}
|
||||
progressBar={undefined}
|
||||
logoUrl={logoUrl}
|
||||
isOuterFooter={true}
|
||||
/>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default CardOuterFooter;
|
104
src/components/cardbuilder/Card/CardOverlayButtons.tsx
Normal file
104
src/components/cardbuilder/Card/CardOverlayButtons.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
||||
import React, { type FC } from 'react';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import classNames from 'classnames';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import PlayArrowIconButton from '../../common/PlayArrowIconButton';
|
||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
const sholudShowOverlayPlayButton = (
|
||||
overlayPlayButton: boolean | undefined,
|
||||
item: ItemDto
|
||||
) => {
|
||||
return (
|
||||
overlayPlayButton
|
||||
&& !item.IsPlaceHolder
|
||||
&& (item.LocationType !== LocationType.Virtual
|
||||
|| !item.MediaType
|
||||
|| item.Type === BaseItemKind.Program)
|
||||
&& item.Type !== BaseItemKind.Person
|
||||
);
|
||||
};
|
||||
|
||||
interface CardOverlayButtonsProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
const CardOverlayButtons: FC<CardOverlayButtonsProps> = ({
|
||||
item,
|
||||
cardOptions
|
||||
}) => {
|
||||
let overlayPlayButton = cardOptions.overlayPlayButton;
|
||||
|
||||
if (
|
||||
overlayPlayButton == null
|
||||
&& !cardOptions.overlayMoreButton
|
||||
&& !cardOptions.overlayInfoButton
|
||||
&& !cardOptions.cardLayout
|
||||
) {
|
||||
overlayPlayButton = item.MediaType === 'Video';
|
||||
}
|
||||
|
||||
const url = appRouter.getRouteUrl(item, {
|
||||
parentId: cardOptions.parentId
|
||||
});
|
||||
|
||||
const btnCssClass = classNames(
|
||||
'paper-icon-button-light',
|
||||
'cardOverlayButton',
|
||||
'itemAction'
|
||||
);
|
||||
|
||||
const centerPlayButtonClass = classNames(
|
||||
btnCssClass,
|
||||
'cardOverlayButton-centered'
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
aria-label={item.Name || ''}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
userSelect: 'none',
|
||||
borderRadius: '0.2em'
|
||||
}}
|
||||
>
|
||||
|
||||
{cardOptions.centerPlayButton && (
|
||||
<PlayArrowIconButton
|
||||
className={centerPlayButtonClass}
|
||||
action='play'
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
|
||||
<ButtonGroup className='cardOverlayButton-br'>
|
||||
{sholudShowOverlayPlayButton(overlayPlayButton, item) && (
|
||||
<PlayArrowIconButton
|
||||
className={btnCssClass}
|
||||
action='play'
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
|
||||
{cardOptions.overlayMoreButton && (
|
||||
<MoreVertIconButton
|
||||
className={btnCssClass}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardOverlayButtons;
|
32
src/components/cardbuilder/Card/CardText.tsx
Normal file
32
src/components/cardbuilder/Card/CardText.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { TextLine } from './cardHelper';
|
||||
|
||||
interface CardTextProps {
|
||||
className?: string;
|
||||
textLine: TextLine;
|
||||
}
|
||||
|
||||
const CardText: FC<CardTextProps> = ({ className, textLine }) => {
|
||||
const { title, titleAction } = textLine;
|
||||
const renderCardText = () => {
|
||||
if (titleAction) {
|
||||
return (
|
||||
<a
|
||||
className='itemAction textActionButton'
|
||||
href={titleAction.url}
|
||||
title={titleAction.title}
|
||||
{...titleAction.dataAttributes}
|
||||
>
|
||||
{titleAction.title}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
};
|
||||
|
||||
return <Box className={className}>{renderCardText()}</Box>;
|
||||
};
|
||||
|
||||
export default CardText;
|
30
src/components/cardbuilder/Card/CardWrapper.tsx
Normal file
30
src/components/cardbuilder/Card/CardWrapper.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { type FC } from 'react';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import type { DataAttributes } from 'types/dataAttributes';
|
||||
|
||||
interface CardWrapperProps {
|
||||
className: string;
|
||||
dataAttributes: DataAttributes;
|
||||
}
|
||||
|
||||
const CardWrapper: FC<CardWrapperProps> = ({
|
||||
className,
|
||||
dataAttributes,
|
||||
children
|
||||
}) => {
|
||||
if (layoutManager.tv) {
|
||||
return (
|
||||
<button className={className} {...dataAttributes}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={className} {...dataAttributes}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CardWrapper;
|
24
src/components/cardbuilder/Card/Cards.tsx
Normal file
24
src/components/cardbuilder/Card/Cards.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { type FC } from 'react';
|
||||
import { setCardData } from '../cardBuilder';
|
||||
import Card from './Card';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
import '../card.scss';
|
||||
|
||||
interface CardsProps {
|
||||
items: ItemDto[];
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
const Cards: FC<CardsProps> = ({ items, cardOptions }) => {
|
||||
setCardData(items, cardOptions);
|
||||
|
||||
const renderCards = () =>
|
||||
items.map((item) => (
|
||||
<Card key={item.Id} item={item} cardOptions={cardOptions} />
|
||||
));
|
||||
|
||||
return <>{renderCards()}</>;
|
||||
};
|
||||
|
||||
export default Cards;
|
723
src/components/cardbuilder/Card/cardHelper.ts
Normal file
723
src/components/cardbuilder/Card/cardHelper.ts
Normal file
|
@ -0,0 +1,723 @@
|
|||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
BaseItemPerson,
|
||||
ImageType
|
||||
} from '@jellyfin/sdk/lib/generated-client';
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import globalize from 'scripts/globalize';
|
||||
import datetime from 'scripts/datetime';
|
||||
|
||||
import { isUsingLiveTvNaming } from '../cardBuilderUtils';
|
||||
|
||||
import type { NullableNumber, NullableString } from 'types/base/common/shared/types';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
import type { DataAttributes } from 'types/dataAttributes';
|
||||
import { getDataAttributes } from 'utils/items';
|
||||
|
||||
export function getCardLogoUrl(
|
||||
item: ItemDto,
|
||||
api: Api | undefined,
|
||||
cardOptions: CardOptions
|
||||
) {
|
||||
let imgType;
|
||||
let imgTag;
|
||||
let itemId;
|
||||
const logoHeight = 40;
|
||||
|
||||
if (cardOptions.showChannelLogo && item.ChannelPrimaryImageTag) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.ChannelPrimaryImageTag;
|
||||
itemId = item.ChannelId;
|
||||
} else if (cardOptions.showLogo && item.ParentLogoImageTag) {
|
||||
imgType = ImageType.Logo;
|
||||
imgTag = item.ParentLogoImageTag;
|
||||
itemId = item.ParentLogoItemId;
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
itemId = item.Id;
|
||||
}
|
||||
|
||||
if (api && imgTag && imgType && itemId) {
|
||||
const response = getImageApi(api).getItemImageUrlById(itemId, imgType, {
|
||||
height: logoHeight,
|
||||
tag: imgTag
|
||||
});
|
||||
|
||||
return {
|
||||
logoUrl: response
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
logoUrl: undefined
|
||||
};
|
||||
}
|
||||
|
||||
interface TextAction {
|
||||
url: string;
|
||||
title: string;
|
||||
dataAttributes: DataAttributes
|
||||
}
|
||||
|
||||
export interface TextLine {
|
||||
title?: NullableString;
|
||||
titleAction?: TextAction;
|
||||
}
|
||||
|
||||
export function getTextActionButton(
|
||||
item: ItemDto,
|
||||
text?: NullableString,
|
||||
serverId?: NullableString
|
||||
): TextLine {
|
||||
if (!text) {
|
||||
text = itemHelper.getDisplayName(item);
|
||||
}
|
||||
|
||||
text = escapeHTML(text);
|
||||
|
||||
if (layoutManager.tv) {
|
||||
return {
|
||||
title: text
|
||||
};
|
||||
}
|
||||
|
||||
const url = appRouter.getRouteUrl(item);
|
||||
|
||||
const dataAttributes = getDataAttributes(
|
||||
{
|
||||
action: 'link',
|
||||
itemServerId: serverId ?? item.ServerId,
|
||||
itemId: item.Id,
|
||||
itemChannelId: item.ChannelId,
|
||||
itemType: item.Type,
|
||||
itemMediaType: item.MediaType,
|
||||
itemCollectionType: item.CollectionType,
|
||||
itemIsFolder: item.IsFolder
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
titleAction: {
|
||||
url,
|
||||
title: text,
|
||||
dataAttributes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getAirTimeText(
|
||||
item: ItemDto,
|
||||
showAirDateTime: boolean | undefined,
|
||||
showAirEndTime: boolean | undefined
|
||||
) {
|
||||
let airTimeText = '';
|
||||
|
||||
if (item.StartDate) {
|
||||
try {
|
||||
let date = datetime.parseISO8601Date(item.StartDate);
|
||||
|
||||
if (showAirDateTime) {
|
||||
airTimeText
|
||||
+= datetime.toLocaleDateString(date, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}) + ' ';
|
||||
}
|
||||
|
||||
airTimeText += datetime.getDisplayTime(date);
|
||||
|
||||
if (item.EndDate && showAirEndTime) {
|
||||
date = datetime.parseISO8601Date(item.EndDate);
|
||||
airTimeText += ' - ' + datetime.getDisplayTime(date);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error parsing date: ' + item.StartDate);
|
||||
}
|
||||
}
|
||||
return airTimeText;
|
||||
}
|
||||
|
||||
function isGenreOrStudio(itemType: NullableString) {
|
||||
return itemType === BaseItemKind.Genre || itemType === BaseItemKind.Studio;
|
||||
}
|
||||
|
||||
function isMusicGenreOrMusicArtist(
|
||||
itemType: NullableString,
|
||||
context: NullableString
|
||||
) {
|
||||
return itemType === BaseItemKind.MusicGenre || context === 'MusicArtist';
|
||||
}
|
||||
|
||||
function getMovieCount(itemMovieCount: NullableNumber) {
|
||||
if (itemMovieCount) {
|
||||
return itemMovieCount === 1 ?
|
||||
globalize.translate('ValueOneMovie') :
|
||||
globalize.translate('ValueMovieCount', itemMovieCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getSeriesCount(itemSeriesCount: NullableNumber) {
|
||||
if (itemSeriesCount) {
|
||||
return itemSeriesCount === 1 ?
|
||||
globalize.translate('ValueOneSeries') :
|
||||
globalize.translate('ValueSeriesCount', itemSeriesCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getEpisodeCount(itemEpisodeCount: NullableNumber) {
|
||||
if (itemEpisodeCount) {
|
||||
return itemEpisodeCount === 1 ?
|
||||
globalize.translate('ValueOneEpisode') :
|
||||
globalize.translate('ValueEpisodeCount', itemEpisodeCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getAlbumCount(itemAlbumCount: NullableNumber) {
|
||||
if (itemAlbumCount) {
|
||||
return itemAlbumCount === 1 ?
|
||||
globalize.translate('ValueOneAlbum') :
|
||||
globalize.translate('ValueAlbumCount', itemAlbumCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getSongCount(itemSongCount: NullableNumber) {
|
||||
if (itemSongCount) {
|
||||
return itemSongCount === 1 ?
|
||||
globalize.translate('ValueOneSong') :
|
||||
globalize.translate('ValueSongCount', itemSongCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getMusicVideoCount(itemMusicVideoCount: NullableNumber) {
|
||||
if (itemMusicVideoCount) {
|
||||
return itemMusicVideoCount === 1 ?
|
||||
globalize.translate('ValueOneMusicVideo') :
|
||||
globalize.translate('ValueMusicVideoCount', itemMusicVideoCount);
|
||||
}
|
||||
}
|
||||
|
||||
function getRecursiveItemCount(itemRecursiveItemCount: NullableNumber) {
|
||||
return itemRecursiveItemCount === 1 ?
|
||||
globalize.translate('ValueOneEpisode') :
|
||||
globalize.translate('ValueEpisodeCount', itemRecursiveItemCount);
|
||||
}
|
||||
|
||||
function getParentTitle(
|
||||
isOuterFooter: boolean,
|
||||
serverId: NullableString,
|
||||
item: ItemDto
|
||||
) {
|
||||
if (isOuterFooter && item.AlbumArtists?.length) {
|
||||
(item.AlbumArtists[0] as BaseItemDto).Type = BaseItemKind.MusicArtist;
|
||||
(item.AlbumArtists[0] as BaseItemDto).IsFolder = true;
|
||||
return getTextActionButton(item.AlbumArtists[0], null, serverId);
|
||||
} else {
|
||||
return {
|
||||
title: isUsingLiveTvNaming(item.Type) ?
|
||||
item.Name :
|
||||
item.SeriesName
|
||||
|| item.Series
|
||||
|| item.Album
|
||||
|| item.AlbumArtist
|
||||
|| ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getRunTimeTicks(itemRunTimeTicks: NullableNumber) {
|
||||
if (itemRunTimeTicks) {
|
||||
let minutes = itemRunTimeTicks / 600000000;
|
||||
|
||||
minutes = minutes || 1;
|
||||
|
||||
return globalize.translate('ValueMinutes', Math.round(minutes));
|
||||
} else {
|
||||
return globalize.translate('ValueMinutes', 0);
|
||||
}
|
||||
}
|
||||
|
||||
export function getItemCounts(cardOptions: CardOptions, item: ItemDto) {
|
||||
const counts: string[] = [];
|
||||
|
||||
const addCount = (text: NullableString) => {
|
||||
if (text) {
|
||||
counts.push(text);
|
||||
}
|
||||
};
|
||||
|
||||
if (item.Type === BaseItemKind.Playlist) {
|
||||
const runTimeTicksText = getRunTimeTicks(item.RunTimeTicks);
|
||||
addCount(runTimeTicksText);
|
||||
} else if (isGenreOrStudio(item.Type)) {
|
||||
const movieCountText = getMovieCount(item.MovieCount);
|
||||
addCount(movieCountText);
|
||||
|
||||
const seriesCountText = getSeriesCount(item.SeriesCount);
|
||||
addCount(seriesCountText);
|
||||
|
||||
const episodeCountText = getEpisodeCount(item.EpisodeCount);
|
||||
addCount(episodeCountText);
|
||||
} else if (isMusicGenreOrMusicArtist(item.Type, cardOptions.context)) {
|
||||
const albumCountText = getAlbumCount(item.AlbumCount);
|
||||
addCount(albumCountText);
|
||||
|
||||
const songCountText = getSongCount(item.SongCount);
|
||||
addCount(songCountText);
|
||||
|
||||
const musicVideoCountText = getMusicVideoCount(item.MusicVideoCount);
|
||||
addCount(musicVideoCountText);
|
||||
} else if (item.Type === BaseItemKind.Series) {
|
||||
const recursiveItemCountText = getRecursiveItemCount(
|
||||
item.RecursiveItemCount
|
||||
);
|
||||
addCount(recursiveItemCountText);
|
||||
}
|
||||
|
||||
return counts.join(', ');
|
||||
}
|
||||
|
||||
export function shouldShowTitle(
|
||||
showTitle: boolean | string | undefined,
|
||||
itemType: NullableString
|
||||
) {
|
||||
return (
|
||||
Boolean(showTitle)
|
||||
|| itemType === BaseItemKind.PhotoAlbum
|
||||
|| itemType === BaseItemKind.Folder
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldShowOtherText(
|
||||
isOuterFooter: boolean,
|
||||
overlayText: boolean | undefined
|
||||
) {
|
||||
return isOuterFooter ? !overlayText : overlayText;
|
||||
}
|
||||
|
||||
export function shouldShowParentTitleUnderneath(
|
||||
itemType: NullableString
|
||||
) {
|
||||
return (
|
||||
itemType === BaseItemKind.MusicAlbum
|
||||
|| itemType === BaseItemKind.Audio
|
||||
|| itemType === BaseItemKind.MusicVideo
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowMediaTitle(
|
||||
titleAdded: boolean,
|
||||
showTitle: boolean,
|
||||
forceName: boolean,
|
||||
cardOptions: CardOptions,
|
||||
textLines: TextLine[]
|
||||
) {
|
||||
let showMediaTitle =
|
||||
(showTitle && !titleAdded)
|
||||
|| (cardOptions.showParentTitleOrTitle && !textLines.length);
|
||||
if (!showMediaTitle && !titleAdded && (showTitle || forceName)) {
|
||||
showMediaTitle = true;
|
||||
}
|
||||
return showMediaTitle;
|
||||
}
|
||||
|
||||
function shouldShowExtraType(itemExtraType: NullableString) {
|
||||
return itemExtraType && itemExtraType !== 'Unknown';
|
||||
}
|
||||
|
||||
function shouldShowSeriesYearOrYear(
|
||||
showYear: string | boolean | undefined,
|
||||
showSeriesYear: boolean | undefined
|
||||
) {
|
||||
return Boolean(showYear) || showSeriesYear;
|
||||
}
|
||||
|
||||
function shouldShowCurrentProgram(
|
||||
showCurrentProgram: boolean | undefined,
|
||||
itemType: NullableString
|
||||
) {
|
||||
return showCurrentProgram && itemType === BaseItemKind.TvChannel;
|
||||
}
|
||||
|
||||
function shouldShowCurrentProgramTime(
|
||||
showCurrentProgramTime: boolean | undefined,
|
||||
itemType: NullableString
|
||||
) {
|
||||
return showCurrentProgramTime && itemType === BaseItemKind.TvChannel;
|
||||
}
|
||||
|
||||
function shouldShowPersonRoleOrType(
|
||||
showPersonRoleOrType: boolean | undefined,
|
||||
item: ItemDto
|
||||
) {
|
||||
return showPersonRoleOrType && (item as BaseItemPerson).Role;
|
||||
}
|
||||
|
||||
function shouldShowParentTitle(
|
||||
showParentTitle: boolean | undefined,
|
||||
parentTitleUnderneath: boolean
|
||||
) {
|
||||
return showParentTitle && parentTitleUnderneath;
|
||||
}
|
||||
|
||||
function addOtherText(
|
||||
cardOptions: CardOptions,
|
||||
parentTitleUnderneath: boolean,
|
||||
isOuterFooter: boolean,
|
||||
item: ItemDto,
|
||||
addTextLine: (val: TextLine) => void,
|
||||
serverId: NullableString
|
||||
) {
|
||||
if (
|
||||
shouldShowParentTitle(
|
||||
cardOptions.showParentTitle,
|
||||
parentTitleUnderneath
|
||||
)
|
||||
) {
|
||||
addTextLine(getParentTitle(isOuterFooter, serverId, item));
|
||||
}
|
||||
|
||||
if (shouldShowExtraType(item.ExtraType)) {
|
||||
addTextLine({ title: globalize.translate(item.ExtraType) });
|
||||
}
|
||||
|
||||
if (cardOptions.showItemCounts) {
|
||||
addTextLine({ title: getItemCounts(cardOptions, item) });
|
||||
}
|
||||
|
||||
if (cardOptions.textLines) {
|
||||
addTextLine({ title: getAdditionalLines(cardOptions.textLines, item) });
|
||||
}
|
||||
|
||||
if (cardOptions.showSongCount) {
|
||||
addTextLine({ title: getSongCount(item.SongCount) });
|
||||
}
|
||||
|
||||
if (cardOptions.showPremiereDate) {
|
||||
addTextLine({ title: getPremiereDate(item.PremiereDate) });
|
||||
}
|
||||
|
||||
if (
|
||||
shouldShowSeriesYearOrYear(
|
||||
cardOptions.showYear,
|
||||
cardOptions.showSeriesYear
|
||||
)
|
||||
) {
|
||||
addTextLine({ title: getProductionYear(item) });
|
||||
}
|
||||
|
||||
if (cardOptions.showRuntime) {
|
||||
addTextLine({ title: getRunTime(item.RunTimeTicks) });
|
||||
}
|
||||
|
||||
if (cardOptions.showAirTime) {
|
||||
addTextLine({
|
||||
title: getAirTimeText(
|
||||
item,
|
||||
cardOptions.showAirDateTime,
|
||||
cardOptions.showAirEndTime
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (cardOptions.showChannelName) {
|
||||
addTextLine(getChannelName(item));
|
||||
}
|
||||
|
||||
if (shouldShowCurrentProgram(cardOptions.showCurrentProgram, item.Type)) {
|
||||
addTextLine({ title: getCurrentProgramName(item.CurrentProgram) });
|
||||
}
|
||||
|
||||
if (
|
||||
shouldShowCurrentProgramTime(
|
||||
cardOptions.showCurrentProgramTime,
|
||||
item.Type
|
||||
)
|
||||
) {
|
||||
addTextLine({ title: getCurrentProgramTime(item.CurrentProgram) });
|
||||
}
|
||||
|
||||
if (cardOptions.showSeriesTimerTime) {
|
||||
addTextLine({ title: getSeriesTimerTime(item) });
|
||||
}
|
||||
|
||||
if (cardOptions.showSeriesTimerChannel) {
|
||||
addTextLine({ title: getSeriesTimerChannel(item) });
|
||||
}
|
||||
|
||||
if (shouldShowPersonRoleOrType(cardOptions.showCurrentProgramTime, item)) {
|
||||
addTextLine({
|
||||
title: globalize.translate(
|
||||
'PersonRole',
|
||||
(item as BaseItemPerson).Role
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSeriesTimerChannel(item: ItemDto) {
|
||||
if (item.RecordAnyChannel) {
|
||||
return globalize.translate('AllChannels');
|
||||
} else {
|
||||
return item.ChannelName || '' || globalize.translate('OneChannel');
|
||||
}
|
||||
}
|
||||
|
||||
function getSeriesTimerTime(item: ItemDto) {
|
||||
if (item.RecordAnyTime) {
|
||||
return globalize.translate('Anytime');
|
||||
} else {
|
||||
return datetime.getDisplayTime(item.StartDate);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentProgramTime(CurrentProgram: BaseItemDto | undefined) {
|
||||
if (CurrentProgram) {
|
||||
return getAirTimeText(CurrentProgram, false, true) || '';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentProgramName(CurrentProgram: BaseItemDto | undefined) {
|
||||
if (CurrentProgram) {
|
||||
return CurrentProgram.Name;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getChannelName(item: ItemDto) {
|
||||
if (item.ChannelId) {
|
||||
return getTextActionButton(
|
||||
{
|
||||
Id: item.ChannelId,
|
||||
ServerId: item.ServerId,
|
||||
Name: item.ChannelName,
|
||||
Type: BaseItemKind.TvChannel,
|
||||
MediaType: item.MediaType,
|
||||
IsFolder: false
|
||||
},
|
||||
item.ChannelName
|
||||
);
|
||||
} else {
|
||||
return { title: item.ChannelName || '' || ' ' };
|
||||
}
|
||||
}
|
||||
|
||||
function getRunTime(itemRunTimeTicks: NullableNumber) {
|
||||
if (itemRunTimeTicks) {
|
||||
return datetime.getDisplayRunningTime(itemRunTimeTicks);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getPremiereDate(PremiereDate: string | null | undefined) {
|
||||
if (PremiereDate) {
|
||||
try {
|
||||
return datetime.toLocaleDateString(
|
||||
datetime.parseISO8601Date(PremiereDate),
|
||||
{ weekday: 'long', month: 'long', day: 'numeric' }
|
||||
);
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getAdditionalLines(
|
||||
textLines: (item: ItemDto) => (string | undefined)[],
|
||||
item: ItemDto
|
||||
) {
|
||||
const additionalLines = textLines(item);
|
||||
for (const additionalLine of additionalLines) {
|
||||
return additionalLine;
|
||||
}
|
||||
}
|
||||
|
||||
function getProductionYear(item: ItemDto) {
|
||||
const productionYear =
|
||||
item.ProductionYear
|
||||
&& datetime.toLocaleString(item.ProductionYear, {
|
||||
useGrouping: false
|
||||
});
|
||||
if (item.Type === BaseItemKind.Series) {
|
||||
if (item.Status === 'Continuing') {
|
||||
return globalize.translate(
|
||||
'SeriesYearToPresent',
|
||||
productionYear || ''
|
||||
);
|
||||
} else if (item.EndDate && item.ProductionYear) {
|
||||
const endYear = datetime.toLocaleString(
|
||||
datetime.parseISO8601Date(item.EndDate).getFullYear(),
|
||||
{ useGrouping: false }
|
||||
);
|
||||
return (
|
||||
productionYear
|
||||
+ (endYear === productionYear ? '' : ' - ' + endYear)
|
||||
);
|
||||
} else {
|
||||
return productionYear || '';
|
||||
}
|
||||
} else {
|
||||
return productionYear || '';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaTitle(cardOptions: CardOptions, item: ItemDto): TextLine {
|
||||
const name =
|
||||
cardOptions.showTitle === 'auto'
|
||||
&& !item.IsFolder
|
||||
&& item.MediaType === 'Photo' ?
|
||||
'' :
|
||||
itemHelper.getDisplayName(item, {
|
||||
includeParentInfo: cardOptions.includeParentInfoInTitle
|
||||
});
|
||||
|
||||
return getTextActionButton({
|
||||
Id: item.Id,
|
||||
ServerId: item.ServerId,
|
||||
Name: name,
|
||||
Type: item.Type,
|
||||
CollectionType: item.CollectionType,
|
||||
IsFolder: item.IsFolder
|
||||
});
|
||||
}
|
||||
|
||||
function getParentTitleOrTitle(
|
||||
isOuterFooter: boolean,
|
||||
item: ItemDto,
|
||||
setTitleAdded: (val: boolean) => void,
|
||||
showTitle: boolean
|
||||
): TextLine {
|
||||
if (
|
||||
isOuterFooter
|
||||
&& item.Type === BaseItemKind.Episode
|
||||
&& item.SeriesName
|
||||
) {
|
||||
if (item.SeriesId) {
|
||||
return getTextActionButton({
|
||||
Id: item.SeriesId,
|
||||
ServerId: item.ServerId,
|
||||
Name: item.SeriesName,
|
||||
Type: BaseItemKind.Series,
|
||||
IsFolder: true
|
||||
});
|
||||
} else {
|
||||
return { title: item.SeriesName };
|
||||
}
|
||||
} else if (isUsingLiveTvNaming(item.Type)) {
|
||||
if (!item.EpisodeTitle && !item.IndexNumber) {
|
||||
setTitleAdded(true);
|
||||
}
|
||||
return { title: item.Name };
|
||||
} else {
|
||||
const parentTitle =
|
||||
item.SeriesName
|
||||
|| item.Series
|
||||
|| item.Album
|
||||
|| item.AlbumArtist
|
||||
|| '';
|
||||
|
||||
if (parentTitle || showTitle) {
|
||||
return { title: parentTitle };
|
||||
}
|
||||
|
||||
return { title: '' };
|
||||
}
|
||||
}
|
||||
|
||||
interface TextLinesOpts {
|
||||
isOuterFooter: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
forceName: boolean;
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
imgUrl: string | undefined;
|
||||
}
|
||||
|
||||
export function getCardTextLines({
|
||||
isOuterFooter,
|
||||
overlayText,
|
||||
forceName,
|
||||
item,
|
||||
cardOptions,
|
||||
imgUrl
|
||||
}: TextLinesOpts) {
|
||||
const showTitle = shouldShowTitle(cardOptions.showTitle, item.Type);
|
||||
const showOtherText = shouldShowOtherText(isOuterFooter, overlayText);
|
||||
const serverId = item.ServerId || cardOptions.serverId;
|
||||
let textLines: TextLine[] = [];
|
||||
const parentTitleUnderneath = shouldShowParentTitleUnderneath(item.Type);
|
||||
|
||||
let titleAdded = false;
|
||||
const addTextLine = (val: TextLine) => {
|
||||
textLines.push(val);
|
||||
};
|
||||
|
||||
const setTitleAdded = (val: boolean) => {
|
||||
titleAdded = val;
|
||||
};
|
||||
|
||||
if (
|
||||
showOtherText
|
||||
&& (cardOptions.showParentTitle || cardOptions.showParentTitleOrTitle)
|
||||
&& !parentTitleUnderneath
|
||||
) {
|
||||
addTextLine(
|
||||
getParentTitleOrTitle(isOuterFooter, item, setTitleAdded, showTitle)
|
||||
);
|
||||
}
|
||||
|
||||
const showMediaTitle = shouldShowMediaTitle(
|
||||
titleAdded,
|
||||
showTitle,
|
||||
forceName,
|
||||
cardOptions,
|
||||
textLines
|
||||
);
|
||||
|
||||
if (showMediaTitle) {
|
||||
addTextLine(getMediaTitle(cardOptions, item));
|
||||
}
|
||||
|
||||
if (showOtherText) {
|
||||
addOtherText(
|
||||
cardOptions,
|
||||
parentTitleUnderneath,
|
||||
isOuterFooter,
|
||||
item,
|
||||
addTextLine,
|
||||
serverId
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(showTitle || !imgUrl)
|
||||
&& forceName
|
||||
&& overlayText
|
||||
&& textLines.length === 1
|
||||
) {
|
||||
textLines = [];
|
||||
}
|
||||
|
||||
if (overlayText && showTitle) {
|
||||
textLines = [{ title: item.Name }];
|
||||
}
|
||||
|
||||
return {
|
||||
textLines
|
||||
};
|
||||
}
|
123
src/components/cardbuilder/Card/useCard.ts
Normal file
123
src/components/cardbuilder/Card/useCard.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import classNames from 'classnames';
|
||||
import useCardImageUrl from './useCardImageUrl';
|
||||
import {
|
||||
resolveAction,
|
||||
resolveMixedShapeByAspectRatio
|
||||
} from '../cardBuilderUtils';
|
||||
import { getDataAttributes } from 'utils/items';
|
||||
import { CardShape } from 'utils/card';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface UseCardProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
|
||||
function useCard({ item, cardOptions }: UseCardProps) {
|
||||
const action = resolveAction({
|
||||
defaultAction: cardOptions.action ?? 'link',
|
||||
isFolder: item.IsFolder ?? false,
|
||||
isPhoto: item.MediaType === 'Photo'
|
||||
});
|
||||
|
||||
let shape = cardOptions.shape;
|
||||
|
||||
if (shape === CardShape.Mixed) {
|
||||
shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio);
|
||||
}
|
||||
|
||||
const imgInfo = useCardImageUrl({
|
||||
item: item.ProgramInfo ?? item,
|
||||
cardOptions,
|
||||
shape
|
||||
});
|
||||
const imgUrl = imgInfo.imgUrl;
|
||||
const blurhash = imgInfo.blurhash;
|
||||
const forceName = imgInfo.forceName;
|
||||
const coveredImage = cardOptions.coverImage ?? imgInfo.coverImage;
|
||||
const overlayText = cardOptions.overlayText;
|
||||
|
||||
const nameWithPrefix = item.SortName ?? item.Name ?? '';
|
||||
let prefix = nameWithPrefix.substring(
|
||||
0,
|
||||
Math.min(3, nameWithPrefix.length)
|
||||
);
|
||||
|
||||
if (prefix) {
|
||||
prefix = prefix.toUpperCase();
|
||||
}
|
||||
|
||||
const dataAttributes = getDataAttributes(
|
||||
{
|
||||
action,
|
||||
itemServerId: item.ServerId ?? cardOptions.serverId,
|
||||
context: cardOptions.context,
|
||||
parentId: cardOptions.parentId,
|
||||
collectionId: cardOptions.collectionId,
|
||||
playlistId: cardOptions.playlistId,
|
||||
itemId: item.Id,
|
||||
itemTimerId: item.TimerId,
|
||||
itemSeriesTimerId: item.SeriesTimerId,
|
||||
itemChannelId: item.ChannelId,
|
||||
itemType: item.Type,
|
||||
itemMediaType: item.MediaType,
|
||||
itemCollectionType: item.CollectionType,
|
||||
itemIsFolder: item.IsFolder,
|
||||
itemPath: item.Path,
|
||||
itemStartDate: item.StartDate,
|
||||
itemEndDate: item.EndDate,
|
||||
itemUserData: item.UserData,
|
||||
prefix
|
||||
}
|
||||
);
|
||||
|
||||
const cardClass = classNames(
|
||||
'card',
|
||||
{ [`${shape}Card`]: shape },
|
||||
cardOptions.cardCssClass,
|
||||
cardOptions.cardClass,
|
||||
{ 'card-hoverable': layoutManager.desktop },
|
||||
{ groupedCard: cardOptions.showChildCountIndicator && item.ChildCount },
|
||||
{
|
||||
'card-withuserdata':
|
||||
item.Type !== BaseItemKind.MusicAlbum
|
||||
&& item.Type !== BaseItemKind.MusicArtist
|
||||
&& item.Type !== BaseItemKind.Audio
|
||||
},
|
||||
{ itemAction: layoutManager.tv }
|
||||
);
|
||||
|
||||
const cardBoxClass = classNames(
|
||||
'cardBox',
|
||||
{ visualCardBox: cardOptions.cardLayout },
|
||||
{ 'cardBox-bottompadded': !cardOptions.cardLayout }
|
||||
);
|
||||
|
||||
const getCardWrapperProps = () => ({
|
||||
className: cardClass,
|
||||
dataAttributes
|
||||
});
|
||||
|
||||
const getCardBoxProps = () => ({
|
||||
item,
|
||||
cardOptions,
|
||||
className: cardBoxClass,
|
||||
shape,
|
||||
imgUrl,
|
||||
blurhash,
|
||||
forceName,
|
||||
coveredImage,
|
||||
overlayText
|
||||
});
|
||||
|
||||
return {
|
||||
getCardWrapperProps,
|
||||
getCardBoxProps
|
||||
};
|
||||
}
|
||||
|
||||
export default useCard;
|
298
src/components/cardbuilder/Card/useCardImageUrl.ts
Normal file
298
src/components/cardbuilder/Card/useCardImageUrl.ts
Normal file
|
@ -0,0 +1,298 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getDesiredAspect } from '../cardBuilderUtils';
|
||||
import { CardShape } from 'utils/card';
|
||||
import type { NullableNumber, NullableString } from 'types/base/common/shared/types';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
function getPreferThumbInfo(item: ItemDto, cardOptions: CardOptions) {
|
||||
let imgType;
|
||||
let itemId;
|
||||
let imgTag;
|
||||
let forceName = false;
|
||||
|
||||
if (item.ImageTags?.Thumb) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.ImageTags.Thumb;
|
||||
itemId = item.Id;
|
||||
} else if (item.SeriesThumbImageTag && cardOptions.inheritThumb !== false) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.SeriesThumbImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (
|
||||
item.ParentThumbItemId
|
||||
&& cardOptions.inheritThumb !== false
|
||||
&& item.MediaType !== 'Photo'
|
||||
) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.ParentThumbImageTag;
|
||||
itemId = item.ParentThumbItemId;
|
||||
} else if (item.BackdropImageTags?.length) {
|
||||
imgType = ImageType.Backdrop;
|
||||
imgTag = item.BackdropImageTags[0];
|
||||
itemId = item.Id;
|
||||
forceName = true;
|
||||
} else if (
|
||||
item.ParentBackdropImageTags?.length
|
||||
&& cardOptions.inheritThumb !== false
|
||||
&& item.Type === BaseItemKind.Episode
|
||||
) {
|
||||
imgType = ImageType.Backdrop;
|
||||
imgTag = item.ParentBackdropImageTags[0];
|
||||
itemId = item.ParentBackdropItemId;
|
||||
}
|
||||
return {
|
||||
itemId: itemId,
|
||||
imgTag: imgTag,
|
||||
imgType: imgType,
|
||||
forceName: forceName
|
||||
};
|
||||
}
|
||||
|
||||
function getPreferLogoInfo(item: ItemDto) {
|
||||
let imgType;
|
||||
let itemId;
|
||||
let imgTag;
|
||||
|
||||
if (item.ImageTags?.Logo) {
|
||||
imgType = ImageType.Logo;
|
||||
imgTag = item.ImageTags.Logo;
|
||||
itemId = item.Id;
|
||||
} else if (item.ParentLogoImageTag && item.ParentLogoItemId) {
|
||||
imgType = ImageType.Logo;
|
||||
imgTag = item.ParentLogoImageTag;
|
||||
itemId = item.ParentLogoItemId;
|
||||
}
|
||||
return {
|
||||
itemId: itemId,
|
||||
imgTag: imgTag,
|
||||
imgType: imgType
|
||||
};
|
||||
}
|
||||
|
||||
function getCalculatedHeight(
|
||||
itemWidth: NullableNumber,
|
||||
itemPrimaryImageAspectRatio: NullableNumber
|
||||
) {
|
||||
if (itemWidth && itemPrimaryImageAspectRatio) {
|
||||
return Math.round(itemWidth / itemPrimaryImageAspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
function isForceName(cardOptions: CardOptions) {
|
||||
return !!(cardOptions.preferThumb && cardOptions.showTitle !== false);
|
||||
}
|
||||
|
||||
function isCoverImage(
|
||||
itemPrimaryImageAspectRatio: NullableNumber,
|
||||
uiAspect: NullableNumber
|
||||
) {
|
||||
if (itemPrimaryImageAspectRatio && uiAspect) {
|
||||
return Math.abs(itemPrimaryImageAspectRatio - uiAspect) / uiAspect <= 0.2;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldShowPreferBanner(
|
||||
imageTagsBanner: NullableString,
|
||||
cardOptions: CardOptions,
|
||||
shape: CardShape | undefined
|
||||
): boolean {
|
||||
return (
|
||||
(cardOptions.preferBanner || shape === CardShape.Banner)
|
||||
&& Boolean(imageTagsBanner)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowPreferDisc(
|
||||
imageTagsDisc: string | undefined,
|
||||
cardOptions: CardOptions
|
||||
): boolean {
|
||||
return cardOptions.preferDisc === true && Boolean(imageTagsDisc);
|
||||
}
|
||||
|
||||
function shouldShowImageTagsPrimary(item: ItemDto): boolean {
|
||||
return (
|
||||
Boolean(item.ImageTags?.Primary) && (item.Type !== BaseItemKind.Episode || item.ChildCount !== 0)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowImageTagsThumb(item: ItemDto): boolean {
|
||||
return item.Type === BaseItemKind.Season && Boolean(item.ImageTags?.Thumb);
|
||||
}
|
||||
|
||||
function shouldShowSeriesThumbImageTag(
|
||||
itemSeriesThumbImageTag: NullableString,
|
||||
cardOptions: CardOptions
|
||||
): boolean {
|
||||
return (
|
||||
Boolean(itemSeriesThumbImageTag) && cardOptions.inheritThumb !== false
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowParentThumbImageTag(
|
||||
itemParentThumbItemId: NullableString,
|
||||
cardOptions: CardOptions
|
||||
): boolean {
|
||||
return (
|
||||
Boolean(itemParentThumbItemId) && cardOptions.inheritThumb !== false
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowParentBackdropImageTags(item: ItemDto): boolean {
|
||||
return Boolean(item.AlbumId) && Boolean(item.AlbumPrimaryImageTag);
|
||||
}
|
||||
|
||||
function shouldShowPreferThumb(itemType: NullableString, cardOptions: CardOptions): boolean {
|
||||
return Boolean(cardOptions.preferThumb) && !(itemType === BaseItemKind.Program || itemType === BaseItemKind.Episode);
|
||||
}
|
||||
|
||||
function getCardImageInfo(
|
||||
item: ItemDto,
|
||||
cardOptions: CardOptions,
|
||||
shape: CardShape | undefined
|
||||
) {
|
||||
const width = cardOptions.width;
|
||||
let height;
|
||||
const primaryImageAspectRatio = item.PrimaryImageAspectRatio;
|
||||
let forceName = false;
|
||||
let imgTag;
|
||||
let coverImage = false;
|
||||
const uiAspect = getDesiredAspect(shape);
|
||||
let imgType;
|
||||
let itemId;
|
||||
|
||||
if (shouldShowPreferThumb(item.Type, cardOptions)) {
|
||||
const preferThumbInfo = getPreferThumbInfo(item, cardOptions);
|
||||
imgType = preferThumbInfo.imgType;
|
||||
imgTag = preferThumbInfo.imgTag;
|
||||
itemId = preferThumbInfo.itemId;
|
||||
forceName = preferThumbInfo.forceName;
|
||||
} else if (shouldShowPreferBanner(item.ImageTags?.Banner, cardOptions, shape)) {
|
||||
imgType = ImageType.Banner;
|
||||
imgTag = item.ImageTags?.Banner;
|
||||
itemId = item.Id;
|
||||
} else if (shouldShowPreferDisc(item.ImageTags?.Disc, cardOptions)) {
|
||||
imgType = ImageType.Disc;
|
||||
imgTag = item.ImageTags?.Disc;
|
||||
itemId = item.Id;
|
||||
} else if (cardOptions.preferLogo) {
|
||||
const preferLogoInfo = getPreferLogoInfo(item);
|
||||
imgType = preferLogoInfo.imgType;
|
||||
imgTag = preferLogoInfo.imgType;
|
||||
itemId = preferLogoInfo.itemId;
|
||||
} else if (shouldShowImageTagsPrimary(item)) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.ImageTags?.Primary;
|
||||
itemId = item.Id;
|
||||
height = getCalculatedHeight(width, primaryImageAspectRatio);
|
||||
forceName = isForceName(cardOptions);
|
||||
coverImage = isCoverImage(primaryImageAspectRatio, uiAspect);
|
||||
} else if (item.SeriesPrimaryImageTag) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.SeriesPrimaryImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (item.PrimaryImageTag) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.PrimaryImageTag;
|
||||
itemId = item.PrimaryImageItemId;
|
||||
height = getCalculatedHeight(width, primaryImageAspectRatio);
|
||||
forceName = isForceName(cardOptions);
|
||||
coverImage = isCoverImage(primaryImageAspectRatio, uiAspect);
|
||||
} else if (item.ParentPrimaryImageTag) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.ParentPrimaryImageTag;
|
||||
itemId = item.ParentPrimaryImageItemId;
|
||||
} else if (shouldShowParentBackdropImageTags(item)) {
|
||||
imgType = ImageType.Primary;
|
||||
imgTag = item.AlbumPrimaryImageTag;
|
||||
itemId = item.AlbumId;
|
||||
height = getCalculatedHeight(width, primaryImageAspectRatio);
|
||||
forceName = isForceName(cardOptions);
|
||||
coverImage = isCoverImage(primaryImageAspectRatio, uiAspect);
|
||||
} else if (shouldShowImageTagsThumb(item)) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.ImageTags?.Thumb;
|
||||
itemId = item.Id;
|
||||
} else if (item.BackdropImageTags?.length) {
|
||||
imgType = ImageType.Backdrop;
|
||||
imgTag = item.BackdropImageTags[0];
|
||||
itemId = item.Id;
|
||||
} else if (shouldShowSeriesThumbImageTag(item.SeriesThumbImageTag, cardOptions)) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.SeriesThumbImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (shouldShowParentThumbImageTag(item.ParentThumbItemId, cardOptions)) {
|
||||
imgType = ImageType.Thumb;
|
||||
imgTag = item.ParentThumbImageTag;
|
||||
itemId = item.ParentThumbItemId;
|
||||
} else if (
|
||||
item.ParentBackdropImageTags?.length
|
||||
&& cardOptions.inheritThumb !== false
|
||||
) {
|
||||
imgType = ImageType.Backdrop;
|
||||
imgTag = item.ParentBackdropImageTags[0];
|
||||
itemId = item.ParentBackdropItemId;
|
||||
}
|
||||
|
||||
return {
|
||||
imgType,
|
||||
imgTag,
|
||||
itemId,
|
||||
width,
|
||||
height,
|
||||
forceName,
|
||||
coverImage
|
||||
};
|
||||
}
|
||||
|
||||
interface UseCardImageUrlProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
shape: CardShape | undefined;
|
||||
}
|
||||
|
||||
function useCardImageUrl({ item, cardOptions, shape }: UseCardImageUrlProps) {
|
||||
const { api } = useApi();
|
||||
const imgInfo = getCardImageInfo(item, cardOptions, shape);
|
||||
|
||||
let width = imgInfo.width;
|
||||
let height = imgInfo.height;
|
||||
const imgTag = imgInfo.imgTag;
|
||||
const imgType = imgInfo.imgType;
|
||||
const itemId = imgInfo.itemId;
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
let imgUrl;
|
||||
let blurhash;
|
||||
|
||||
if (api && imgTag && imgType && itemId) {
|
||||
if (width) {
|
||||
width = Math.round(width * ratio);
|
||||
}
|
||||
|
||||
if (height) {
|
||||
height = Math.round(height * ratio);
|
||||
}
|
||||
imgUrl = getImageApi(api).getItemImageUrlById(itemId, imgType, {
|
||||
quality: 96,
|
||||
fillWidth: width,
|
||||
fillHeight: height,
|
||||
tag: imgTag
|
||||
});
|
||||
|
||||
blurhash = item?.ImageBlurHashes?.[imgType]?.[imgTag];
|
||||
}
|
||||
|
||||
return {
|
||||
imgUrl: imgUrl,
|
||||
blurhash: blurhash,
|
||||
forceName: imgInfo.forceName,
|
||||
coverImage: imgInfo.coverImage
|
||||
};
|
||||
}
|
||||
|
||||
export default useCardImageUrl;
|
113
src/components/cardbuilder/Card/useCardText.tsx
Normal file
113
src/components/cardbuilder/Card/useCardText.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import classNames from 'classnames';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import CardText from './CardText';
|
||||
import { getCardTextLines } from './cardHelper';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
const enableRightMargin = (
|
||||
isOuterFooter: boolean,
|
||||
cardLayout: boolean | null | undefined,
|
||||
centerText: boolean | undefined,
|
||||
cardFooterAside: string | undefined
|
||||
) => {
|
||||
return (
|
||||
isOuterFooter
|
||||
&& cardLayout
|
||||
&& !centerText
|
||||
&& cardFooterAside !== 'none'
|
||||
&& layoutManager.mobile
|
||||
);
|
||||
};
|
||||
|
||||
interface UseCardTextProps {
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
forceName: boolean;
|
||||
overlayText: boolean | undefined;
|
||||
imgUrl: string | undefined;
|
||||
isOuterFooter: boolean;
|
||||
cssClass: string;
|
||||
forceLines: boolean;
|
||||
maxLines: number | undefined;
|
||||
}
|
||||
|
||||
function useCardText({
|
||||
item,
|
||||
cardOptions,
|
||||
forceName,
|
||||
imgUrl,
|
||||
overlayText,
|
||||
isOuterFooter,
|
||||
cssClass,
|
||||
forceLines,
|
||||
maxLines
|
||||
}: UseCardTextProps) {
|
||||
const { textLines } = getCardTextLines({
|
||||
isOuterFooter,
|
||||
overlayText,
|
||||
forceName,
|
||||
item,
|
||||
cardOptions,
|
||||
imgUrl
|
||||
});
|
||||
|
||||
const addRightMargin = enableRightMargin(
|
||||
isOuterFooter,
|
||||
cardOptions.cardLayout,
|
||||
cardOptions.centerText,
|
||||
cardOptions.cardFooterAside
|
||||
);
|
||||
|
||||
const renderCardTextLines = () => {
|
||||
const components: React.ReactNode[] = [];
|
||||
let valid = 0;
|
||||
for (const textLine of textLines) {
|
||||
const currentCssClass = classNames(
|
||||
cssClass,
|
||||
{
|
||||
'cardText-secondary':
|
||||
valid > 0 && isOuterFooter
|
||||
},
|
||||
{ 'cardText-first': valid === 0 && isOuterFooter },
|
||||
{ 'cardText-rightmargin': addRightMargin }
|
||||
);
|
||||
|
||||
if (textLine) {
|
||||
components.push(
|
||||
<CardText key={valid} className={currentCssClass} textLine={textLine} />
|
||||
);
|
||||
|
||||
valid++;
|
||||
if (maxLines && valid >= maxLines) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (forceLines) {
|
||||
const linesLength = maxLines ?? Math.min(textLines.length, maxLines ?? textLines.length);
|
||||
while (valid < linesLength) {
|
||||
components.push(
|
||||
<Box key={valid} className={cssClass}>
|
||||
|
||||
</Box>
|
||||
);
|
||||
valid++;
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
};
|
||||
|
||||
const cardTextLines = renderCardTextLines();
|
||||
|
||||
return {
|
||||
cardTextLines
|
||||
};
|
||||
}
|
||||
|
||||
export default useCardText;
|
|
@ -378,7 +378,7 @@ button::-moz-focus-inner {
|
|||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.cardDefaultText {
|
||||
.cardImageContainer > .cardDefaultText {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
|
@ -408,6 +408,7 @@ button::-moz-focus-inner {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
contain: layout style;
|
||||
z-index: 1;
|
||||
|
||||
[dir="ltr"] & {
|
||||
right: 0.225em;
|
||||
|
@ -852,7 +853,7 @@ button::-moz-focus-inner {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.cardOverlayFab-primary {
|
||||
.cardOverlayContainer > .cardOverlayFab-primary {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 130%;
|
||||
padding: 0;
|
||||
|
@ -865,7 +866,7 @@ button::-moz-focus-inner {
|
|||
left: 50%;
|
||||
}
|
||||
|
||||
.cardOverlayFab-primary:hover {
|
||||
.cardOverlayContainer > .cardOverlayFab-primary:hover {
|
||||
transform: scale(1.4, 1.4);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
|||
* @param {Object} items - A set of items.
|
||||
* @param {Object} options - Options for handling the items.
|
||||
*/
|
||||
function setCardData(items, options) {
|
||||
export function setCardData(items, options) {
|
||||
options.shape = options.shape || 'auto';
|
||||
|
||||
const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CardShape } from '../../utils/card';
|
||||
import { randomInt } from '../../utils/number';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -10,10 +11,10 @@ const ASPECT_RATIOS = {
|
|||
|
||||
/**
|
||||
* Determines if the item is live TV.
|
||||
* @param {string} itemType - Item type to use for the check.
|
||||
* @param {string | null | undefined} itemType - Item type to use for the check.
|
||||
* @returns {boolean} Flag showing if the item is live TV.
|
||||
*/
|
||||
export const isUsingLiveTvNaming = (itemType: string): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording';
|
||||
export const isUsingLiveTvNaming = (itemType: string | null | undefined): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording';
|
||||
|
||||
/**
|
||||
* Resolves Card action to display
|
||||
|
@ -54,15 +55,15 @@ export const isResizable = (windowWidth: number): boolean => {
|
|||
*/
|
||||
export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number | null | undefined) => {
|
||||
if (primaryImageAspectRatio === undefined || primaryImageAspectRatio === null) {
|
||||
return 'mixedSquare';
|
||||
return CardShape.MixedSquare;
|
||||
}
|
||||
|
||||
if (primaryImageAspectRatio >= 1.33) {
|
||||
return 'mixedBackdrop';
|
||||
return CardShape.MixedBackdrop;
|
||||
} else if (primaryImageAspectRatio > 0.71) {
|
||||
return 'mixedSquare';
|
||||
return CardShape.MixedSquare;
|
||||
} else {
|
||||
return 'mixedPortrait';
|
||||
return CardShape.MixedPortrait;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
56
src/components/common/DefaultIconText.tsx
Normal file
56
src/components/common/DefaultIconText.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { type FC } from 'react';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import imageHelper from 'utils/image';
|
||||
import DefaultName from './DefaultName';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
|
||||
interface DefaultIconTextProps {
|
||||
item: ItemDto;
|
||||
defaultCardImageIcon?: string;
|
||||
}
|
||||
|
||||
const DefaultIconText: FC<DefaultIconTextProps> = ({
|
||||
item,
|
||||
defaultCardImageIcon
|
||||
}) => {
|
||||
if (item.CollectionType) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{imageHelper.getLibraryIcon(item.CollectionType)}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.Type && !(item.Type === BaseItemKind.TvChannel || item.Type === BaseItemKind.Studio )) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{imageHelper.getItemTypeIcon(item.Type)}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
if (defaultCardImageIcon) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{defaultCardImageIcon}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
return <DefaultName item={item} />;
|
||||
};
|
||||
|
||||
export default DefaultIconText;
|
22
src/components/common/DefaultName.tsx
Normal file
22
src/components/common/DefaultName.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import { isUsingLiveTvNaming } from '../cardbuilder/cardBuilderUtils';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
|
||||
interface DefaultNameProps {
|
||||
item: ItemDto;
|
||||
}
|
||||
|
||||
const DefaultName: FC<DefaultNameProps> = ({ item }) => {
|
||||
const defaultName = isUsingLiveTvNaming(item.Type) ?
|
||||
item.Name :
|
||||
itemHelper.getDisplayName(item);
|
||||
return (
|
||||
<Box className='cardText cardDefaultText'>
|
||||
{defaultName}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultName;
|
67
src/components/common/Image.tsx
Normal file
67
src/components/common/Image.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import React, { type FC, useCallback, useState } from 'react';
|
||||
import { BlurhashCanvas } from 'react-blurhash';
|
||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||
|
||||
const imageStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 0
|
||||
};
|
||||
|
||||
interface ImageProps {
|
||||
imgUrl: string;
|
||||
blurhash?: string;
|
||||
containImage: boolean;
|
||||
}
|
||||
|
||||
const Image: FC<ImageProps> = ({
|
||||
imgUrl,
|
||||
blurhash,
|
||||
containImage
|
||||
}) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isLoadStarted, setIsLoadStarted] = useState(false);
|
||||
const handleLoad = useCallback(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleLoadStarted = useCallback(() => {
|
||||
setIsLoadStarted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoaded && isLoadStarted && blurhash && (
|
||||
<BlurhashCanvas
|
||||
hash={blurhash}
|
||||
width= {20}
|
||||
height={20}
|
||||
punch={1}
|
||||
style={{
|
||||
...imageStyle,
|
||||
borderRadius: '0.2em',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LazyLoadImage
|
||||
key={imgUrl}
|
||||
src={imgUrl}
|
||||
style={{
|
||||
...imageStyle,
|
||||
objectFit: containImage ? 'contain' : 'cover'
|
||||
}}
|
||||
onLoad={handleLoad}
|
||||
beforeLoad={handleLoadStarted}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
22
src/components/common/InfoIconButton.tsx
Normal file
22
src/components/common/InfoIconButton.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React, { type FC } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface InfoIconButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const InfoIconButton: FC<InfoIconButtonProps> = ({ className }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='link'
|
||||
title={globalize.translate('ButtonInfo')}
|
||||
>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoIconButton;
|
36
src/components/common/Media.tsx
Normal file
36
src/components/common/Media.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { type FC } from 'react';
|
||||
import Image from './Image';
|
||||
import DefaultIconText from './DefaultIconText';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
|
||||
interface MediaProps {
|
||||
item: ItemDto;
|
||||
imgUrl: string | undefined;
|
||||
blurhash: string | undefined;
|
||||
imageType?: ImageType
|
||||
defaultCardImageIcon?: string
|
||||
}
|
||||
|
||||
const Media: FC<MediaProps> = ({
|
||||
item,
|
||||
imgUrl,
|
||||
blurhash,
|
||||
imageType,
|
||||
defaultCardImageIcon
|
||||
}) => {
|
||||
return imgUrl ? (
|
||||
<Image
|
||||
imgUrl={imgUrl}
|
||||
blurhash={blurhash}
|
||||
containImage={item.Type === BaseItemKind.TvChannel || imageType === ImageType.Logo}
|
||||
/>
|
||||
) : (
|
||||
<DefaultIconText
|
||||
item={item}
|
||||
defaultCardImageIcon={defaultCardImageIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Media;
|
23
src/components/common/MoreVertIconButton.tsx
Normal file
23
src/components/common/MoreVertIconButton.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React, { type FC } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface MoreVertIconButtonProps {
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
const MoreVertIconButton: FC<MoreVertIconButtonProps> = ({ className, iconClassName }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='menu'
|
||||
title={globalize.translate('ButtonMore')}
|
||||
>
|
||||
<MoreVertIcon className={iconClassName} />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreVertIconButton;
|
25
src/components/common/NoItemsMessage.tsx
Normal file
25
src/components/common/NoItemsMessage.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface NoItemsMessageProps {
|
||||
noItemsMessage?: string;
|
||||
}
|
||||
|
||||
const NoItemsMessage: FC<NoItemsMessageProps> = ({
|
||||
noItemsMessage = 'MessageNoItemsAvailable'
|
||||
}) => {
|
||||
return (
|
||||
<Box className='noItemsMessage centerMessage'>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('MessageNothingHere')}
|
||||
</Typography>
|
||||
<Typography paragraph variant='h2'>
|
||||
{globalize.translate(noItemsMessage)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoItemsMessage;
|
25
src/components/common/PlayArrowIconButton.tsx
Normal file
25
src/components/common/PlayArrowIconButton.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { type FC } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface PlayArrowIconButtonProps {
|
||||
className: string;
|
||||
action: string;
|
||||
title: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
const PlayArrowIconButton: FC<PlayArrowIconButtonProps> = ({ className, action, title, iconClassName }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action={action}
|
||||
title={globalize.translate(title)}
|
||||
>
|
||||
<PlayArrowIcon className={iconClassName} />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayArrowIconButton;
|
22
src/components/common/PlaylistAddIconButton.tsx
Normal file
22
src/components/common/PlaylistAddIconButton.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React, { type FC } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface PlaylistAddIconButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PlaylistAddIconButton: FC<PlaylistAddIconButtonProps> = ({ className }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='addtoplaylist'
|
||||
title={globalize.translate('AddToPlaylist')}
|
||||
>
|
||||
<PlaylistAddIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistAddIconButton;
|
24
src/components/common/RightIconButtons.tsx
Normal file
24
src/components/common/RightIconButtons.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { type FC } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
interface RightIconButtonsProps {
|
||||
className?: string;
|
||||
id: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const RightIconButtons: FC<RightIconButtonsProps> = ({ className, id, title, icon }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='custom'
|
||||
data-customaction={id}
|
||||
title={title}
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightIconButtons;
|
|
@ -5,6 +5,14 @@
|
|||
height: 0.28em;
|
||||
}
|
||||
|
||||
.itemLinearProgress {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.itemProgressBarForeground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
261
src/components/indicators/useIndicator.tsx
Normal file
261
src/components/indicators/useIndicator.tsx
Normal file
|
@ -0,0 +1,261 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import LinearProgress, {
|
||||
linearProgressClasses
|
||||
} from '@mui/material/LinearProgress';
|
||||
import FiberSmartRecordIcon from '@mui/icons-material/FiberSmartRecord';
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import VideocamIcon from '@mui/icons-material/Videocam';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import PhotoAlbumIcon from '@mui/icons-material/PhotoAlbum';
|
||||
import PhotoIcon from '@mui/icons-material/Photo';
|
||||
import classNames from 'classnames';
|
||||
import datetime from 'scripts/datetime';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import AutoTimeProgressBar from 'elements/emby-progressbar/AutoTimeProgressBar';
|
||||
import type { NullableString } from 'types/base/common/shared/types';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ProgressOptions } from 'types/progressOptions';
|
||||
|
||||
const TypeIcon = {
|
||||
Video: <VideocamIcon className='indicatorIcon' />,
|
||||
Folder: <FolderIcon className='indicatorIcon' />,
|
||||
PhotoAlbum: <PhotoAlbumIcon className='indicatorIcon' />,
|
||||
Photo: <PhotoIcon className='indicatorIcon' />
|
||||
};
|
||||
|
||||
const getTypeIcon = (itemType: NullableString) => {
|
||||
return TypeIcon[itemType as keyof typeof TypeIcon];
|
||||
};
|
||||
|
||||
const enableProgressIndicator = (
|
||||
itemType: NullableString,
|
||||
itemMediaType: NullableString
|
||||
) => {
|
||||
return (
|
||||
(itemMediaType === 'Video' && itemType !== BaseItemKind.TvChannel)
|
||||
|| itemType === BaseItemKind.AudioBook
|
||||
|| itemType === 'AudioPodcast'
|
||||
);
|
||||
};
|
||||
|
||||
const enableAutoTimeProgressIndicator = (
|
||||
itemType: NullableString,
|
||||
itemStartDate: NullableString,
|
||||
itemEndDate: NullableString
|
||||
) => {
|
||||
return (
|
||||
(itemType === BaseItemKind.Program
|
||||
|| itemType === 'Timer'
|
||||
|| itemType === BaseItemKind.Recording)
|
||||
&& Boolean(itemStartDate)
|
||||
&& Boolean(itemEndDate)
|
||||
);
|
||||
};
|
||||
|
||||
const enablePlayedIndicator = (item: ItemDto) => {
|
||||
return itemHelper.canMarkPlayed(item);
|
||||
};
|
||||
|
||||
const useIndicator = (item: ItemDto) => {
|
||||
const getMediaSourceIndicator = () => {
|
||||
const mediaSourceCount = item.MediaSourceCount ?? 0;
|
||||
if (mediaSourceCount > 1) {
|
||||
return <Box className='mediaSourceIndicator'>{mediaSourceCount}</Box>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMissingIndicator = () => {
|
||||
if (
|
||||
item.Type === BaseItemKind.Episode
|
||||
&& item.LocationType === LocationType.Virtual
|
||||
) {
|
||||
if (item.PremiereDate) {
|
||||
try {
|
||||
const premiereDate = datetime
|
||||
.parseISO8601Date(item.PremiereDate)
|
||||
.getTime();
|
||||
if (premiereDate > new Date().getTime()) {
|
||||
return <Box className='unairedIndicator'>Unaired</Box>;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
return <Box className='missingIndicator'>Missing</Box>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTimerIndicator = (className?: string) => {
|
||||
const indicatorIconClass = classNames('timerIndicator', className);
|
||||
|
||||
let status;
|
||||
|
||||
if (item.Type === 'SeriesTimer') {
|
||||
return <FiberSmartRecordIcon className={indicatorIconClass} />;
|
||||
} else if (item.TimerId || item.SeriesTimerId) {
|
||||
status = item.Status || 'Cancelled';
|
||||
} else if (item.Type === 'Timer') {
|
||||
status = item.Status;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.SeriesTimerId) {
|
||||
return (
|
||||
<FiberSmartRecordIcon
|
||||
className={`${indicatorIconClass} ${
|
||||
status === 'Cancelled' ? 'timerIndicator-inactive' : ''
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <FiberManualRecordIcon className={indicatorIconClass} />;
|
||||
};
|
||||
|
||||
const getTypeIndicator = () => {
|
||||
const icon = getTypeIcon(item.Type);
|
||||
if (icon) {
|
||||
return <Box className='indicator videoIndicator'>{icon}</Box>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getChildCountIndicator = () => {
|
||||
const childCount = item.ChildCount ?? 0;
|
||||
|
||||
if (childCount > 1) {
|
||||
return (
|
||||
<Box className='countIndicator indicator childCountIndicator'>
|
||||
{datetime.toLocaleString(item.ChildCount)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPlayedIndicator = () => {
|
||||
if (enablePlayedIndicator(item)) {
|
||||
const userData = item.UserData || {};
|
||||
if (userData.UnplayedItemCount) {
|
||||
return (
|
||||
<Box className='countIndicator indicator unplayedItemCount'>
|
||||
{datetime.toLocaleString(userData.UnplayedItemCount)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(userData.PlayedPercentage
|
||||
&& userData.PlayedPercentage >= 100)
|
||||
|| userData.Played
|
||||
) {
|
||||
return (
|
||||
<Box className='playedIndicator indicator'>
|
||||
<CheckIcon className='indicatorIcon' />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getProgress = (pct: number, progressOptions?: ProgressOptions) => {
|
||||
const progressBarClass = classNames(
|
||||
'itemLinearProgress',
|
||||
progressOptions?.containerClass
|
||||
);
|
||||
|
||||
return (
|
||||
<LinearProgress
|
||||
className={progressBarClass}
|
||||
variant='determinate'
|
||||
value={pct}
|
||||
sx={{
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#00a4dc'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getProgressBar = (progressOptions?: ProgressOptions) => {
|
||||
if (
|
||||
enableProgressIndicator(item.Type, item.MediaType)
|
||||
&& item.Type !== BaseItemKind.Recording
|
||||
) {
|
||||
const playedPercentage = progressOptions?.userData?.PlayedPercentage ?
|
||||
progressOptions.userData.PlayedPercentage :
|
||||
item?.UserData?.PlayedPercentage;
|
||||
if (playedPercentage && playedPercentage < 100) {
|
||||
return getProgress(playedPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
enableAutoTimeProgressIndicator(
|
||||
item.Type,
|
||||
item.StartDate,
|
||||
item.EndDate
|
||||
)
|
||||
) {
|
||||
let startDate = 0;
|
||||
let endDate = 1;
|
||||
|
||||
try {
|
||||
startDate = datetime.parseISO8601Date(item.StartDate).getTime();
|
||||
endDate = datetime.parseISO8601Date(item.EndDate).getTime();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
const total = endDate - startDate;
|
||||
const pct = 100 * ((now - startDate) / total);
|
||||
|
||||
if (pct > 0 && pct < 100) {
|
||||
const isRecording =
|
||||
item.Type === 'Timer'
|
||||
|| item.Type === BaseItemKind.Recording
|
||||
|| Boolean(item.TimerId);
|
||||
return (
|
||||
<AutoTimeProgressBar
|
||||
pct={pct}
|
||||
progressOptions={progressOptions}
|
||||
isRecording={isRecording}
|
||||
starTtime={startDate}
|
||||
endTtime={endDate}
|
||||
dataAutoMode='time'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
getProgress,
|
||||
getProgressBar,
|
||||
getMediaSourceIndicator,
|
||||
getMissingIndicator,
|
||||
getTimerIndicator,
|
||||
getTypeIndicator,
|
||||
getChildCountIndicator,
|
||||
getPlayedIndicator
|
||||
};
|
||||
};
|
||||
|
||||
export default useIndicator;
|
32
src/components/listview/List/List.tsx
Normal file
32
src/components/listview/List/List.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React, { type FC } from 'react';
|
||||
import useList from './useList';
|
||||
import ListContent from './ListContent';
|
||||
import ListWrapper from './ListWrapper';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
import '../../mediainfo/mediainfo.scss';
|
||||
import '../../guide/programs.scss';
|
||||
|
||||
interface ListProps {
|
||||
index: number;
|
||||
item: ItemDto;
|
||||
listOptions?: ListOptions;
|
||||
}
|
||||
|
||||
const List: FC<ListProps> = ({ index, item, listOptions = {} }) => {
|
||||
const { getListdWrapperProps, getListContentProps } = useList({ item, listOptions } );
|
||||
const listWrapperProps = getListdWrapperProps();
|
||||
const listContentProps = getListContentProps();
|
||||
|
||||
return (
|
||||
<ListWrapper
|
||||
key={index}
|
||||
index={index}
|
||||
{...listWrapperProps}
|
||||
>
|
||||
<ListContent {...listContentProps} />
|
||||
</ListWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
106
src/components/listview/List/ListContent.tsx
Normal file
106
src/components/listview/List/ListContent.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { type FC } from 'react';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import useIndicator from 'components/indicators/useIndicator';
|
||||
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
|
||||
import ListContentWrapper from './ListContentWrapper';
|
||||
import ListItemBody from './ListItemBody';
|
||||
import ListImageContainer from './ListImageContainer';
|
||||
import ListViewUserDataButtons from './ListViewUserDataButtons';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
interface ListContentProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
enableContentWrapper?: boolean;
|
||||
enableOverview?: boolean;
|
||||
enableSideMediaInfo?: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
action?: string;
|
||||
isLargeStyle: boolean;
|
||||
downloadWidth?: number;
|
||||
}
|
||||
|
||||
const ListContent: FC<ListContentProps> = ({
|
||||
item,
|
||||
listOptions,
|
||||
enableContentWrapper,
|
||||
enableOverview,
|
||||
enableSideMediaInfo,
|
||||
clickEntireItem,
|
||||
action,
|
||||
isLargeStyle,
|
||||
downloadWidth
|
||||
}) => {
|
||||
const indicator = useIndicator(item);
|
||||
return (
|
||||
<ListContentWrapper
|
||||
itemOverview={item.Overview}
|
||||
enableContentWrapper={enableContentWrapper}
|
||||
enableOverview={enableOverview}
|
||||
>
|
||||
|
||||
{!clickEntireItem && listOptions.dragHandle && (
|
||||
<DragHandleIcon className='listViewDragHandle listItemIcon listItemIcon-transparent' />
|
||||
)}
|
||||
|
||||
{listOptions.image !== false && (
|
||||
<ListImageContainer
|
||||
item={item}
|
||||
listOptions={listOptions}
|
||||
action={action}
|
||||
isLargeStyle={isLargeStyle}
|
||||
clickEntireItem={clickEntireItem}
|
||||
downloadWidth={downloadWidth}
|
||||
/>
|
||||
)}
|
||||
|
||||
{listOptions.showIndexNumberLeft && (
|
||||
<Box className='listItem-indexnumberleft'>
|
||||
{item.IndexNumber ?? <span> </span>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ListItemBody
|
||||
item={item}
|
||||
listOptions={listOptions}
|
||||
action={action}
|
||||
enableContentWrapper={enableContentWrapper}
|
||||
enableOverview={enableOverview}
|
||||
enableSideMediaInfo={enableSideMediaInfo}
|
||||
getMissingIndicator={indicator.getMissingIndicator}
|
||||
/>
|
||||
|
||||
{listOptions.mediaInfo !== false && enableSideMediaInfo && (
|
||||
<PrimaryMediaInfo
|
||||
className='secondary listItemMediaInfo'
|
||||
item={item}
|
||||
isRuntimeEnabled={true}
|
||||
isStarRatingEnabled={true}
|
||||
isCaptionIndicatorEnabled={true}
|
||||
isEpisodeTitleEnabled={true}
|
||||
isOfficialRatingEnabled={true}
|
||||
getMissingIndicator={indicator.getMissingIndicator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{listOptions.recordButton
|
||||
&& (item.Type === 'Timer' || item.Type === BaseItemKind.Program) && (
|
||||
indicator.getTimerIndicator('listItemAside')
|
||||
)}
|
||||
|
||||
{!clickEntireItem && (
|
||||
<ListViewUserDataButtons
|
||||
item={item}
|
||||
listOptions={listOptions}
|
||||
/>
|
||||
)}
|
||||
</ListContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListContent;
|
34
src/components/listview/List/ListContentWrapper.tsx
Normal file
34
src/components/listview/List/ListContentWrapper.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface ListContentWrapperProps {
|
||||
itemOverview: string | null | undefined;
|
||||
enableContentWrapper?: boolean;
|
||||
enableOverview?: boolean;
|
||||
}
|
||||
|
||||
const ListContentWrapper: FC<ListContentWrapperProps> = ({
|
||||
itemOverview,
|
||||
enableContentWrapper,
|
||||
enableOverview,
|
||||
children
|
||||
}) => {
|
||||
if (enableContentWrapper) {
|
||||
return (
|
||||
<>
|
||||
<Box className='listItem-content'>{children}</Box>
|
||||
|
||||
{enableOverview && itemOverview && (
|
||||
<Box className='listItem-bottomoverview secondary'>
|
||||
<bdi>{itemOverview}</bdi>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
};
|
||||
|
||||
export default ListContentWrapper;
|
30
src/components/listview/List/ListGroupHeaderWrapper.tsx
Normal file
30
src/components/listview/List/ListGroupHeaderWrapper.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
interface ListGroupHeaderWrapperProps {
|
||||
index?: number;
|
||||
}
|
||||
|
||||
const ListGroupHeaderWrapper: FC<ListGroupHeaderWrapperProps> = ({
|
||||
index,
|
||||
children
|
||||
}) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Typography
|
||||
className='listGroupHeader listGroupHeader-first'
|
||||
variant='h2'
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Typography className='listGroupHeader' variant='h2'>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ListGroupHeaderWrapper;
|
103
src/components/listview/List/ListImageContainer.tsx
Normal file
103
src/components/listview/List/ListImageContainer.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import useIndicator from '../../indicators/useIndicator';
|
||||
import layoutManager from '../../layoutManager';
|
||||
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
|
||||
import {
|
||||
canResume,
|
||||
getChannelImageUrl,
|
||||
getImageUrl
|
||||
} from './listHelper';
|
||||
|
||||
import Media from 'components/common/Media';
|
||||
import PlayArrowIconButton from 'components/common/PlayArrowIconButton';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
interface ListImageContainerProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
action?: string | null;
|
||||
isLargeStyle: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
downloadWidth?: number;
|
||||
}
|
||||
|
||||
const ListImageContainer: FC<ListImageContainerProps> = ({
|
||||
item = {},
|
||||
listOptions,
|
||||
action,
|
||||
isLargeStyle,
|
||||
clickEntireItem,
|
||||
downloadWidth
|
||||
}) => {
|
||||
const { api } = useApi();
|
||||
const { getMediaSourceIndicator, getProgressBar, getPlayedIndicator } = useIndicator(item);
|
||||
const imgInfo = listOptions.imageSource === 'channel' ?
|
||||
getChannelImageUrl(item, api, downloadWidth) :
|
||||
getImageUrl(item, api, downloadWidth);
|
||||
|
||||
const defaultCardImageIcon = listOptions.defaultCardImageIcon;
|
||||
const disableIndicators = listOptions.disableIndicators;
|
||||
const imgUrl = imgInfo?.imgUrl;
|
||||
const blurhash = imgInfo.blurhash;
|
||||
|
||||
const imageClass = classNames(
|
||||
'listItemImage',
|
||||
{ 'listItemImage-large': isLargeStyle },
|
||||
{ 'listItemImage-channel': listOptions.imageSource === 'channel' },
|
||||
{ 'listItemImage-large-tv': isLargeStyle && layoutManager.tv },
|
||||
{ itemAction: !clickEntireItem },
|
||||
{ [getDefaultBackgroundClass(item.Name)]: !imgUrl }
|
||||
);
|
||||
|
||||
const playOnImageClick = listOptions.imagePlayButton && !layoutManager.tv;
|
||||
|
||||
const imageAction = playOnImageClick ? 'link' : action;
|
||||
|
||||
const btnCssClass =
|
||||
'paper-icon-button-light listItemImageButton itemAction';
|
||||
|
||||
const mediaSourceIndicator = getMediaSourceIndicator();
|
||||
const playedIndicator = getPlayedIndicator();
|
||||
const progressBar = getProgressBar();
|
||||
const playbackPositionTicks = item?.UserData?.PlaybackPositionTicks;
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-action={imageAction}
|
||||
className={imageClass}
|
||||
>
|
||||
|
||||
<Media item={item} imgUrl={imgUrl} blurhash={blurhash} defaultCardImageIcon={defaultCardImageIcon} />
|
||||
|
||||
{disableIndicators !== true && mediaSourceIndicator}
|
||||
|
||||
{playedIndicator && (
|
||||
<Box className='indicators listItemIndicators'>
|
||||
{playedIndicator}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{playOnImageClick && (
|
||||
<PlayArrowIconButton
|
||||
className={btnCssClass}
|
||||
action={
|
||||
canResume(playbackPositionTicks) ? 'resume' : 'play'
|
||||
}
|
||||
title={
|
||||
canResume(playbackPositionTicks) ?
|
||||
'ButtonResume' :
|
||||
'Play'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{progressBar}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListImageContainer;
|
65
src/components/listview/List/ListItemBody.tsx
Normal file
65
src/components/listview/List/ListItemBody.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
import useListTextlines from './useListTextlines';
|
||||
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
interface ListItemBodyProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
action?: string | null;
|
||||
isLargeStyle?: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
enableContentWrapper?: boolean;
|
||||
enableOverview?: boolean;
|
||||
enableSideMediaInfo?: boolean;
|
||||
getMissingIndicator: () => React.JSX.Element | null
|
||||
}
|
||||
|
||||
const ListItemBody: FC<ListItemBodyProps> = ({
|
||||
item = {},
|
||||
listOptions = {},
|
||||
action,
|
||||
isLargeStyle,
|
||||
clickEntireItem,
|
||||
enableContentWrapper,
|
||||
enableOverview,
|
||||
enableSideMediaInfo,
|
||||
getMissingIndicator
|
||||
}) => {
|
||||
const { listTextLines } = useListTextlines({ item, listOptions, isLargeStyle });
|
||||
const cssClass = classNames(
|
||||
'listItemBody',
|
||||
{ 'itemAction': !clickEntireItem },
|
||||
{ 'listItemBody-noleftpadding': listOptions.image === false }
|
||||
);
|
||||
|
||||
return (
|
||||
<Box data-action={action} className={cssClass}>
|
||||
|
||||
{listTextLines}
|
||||
|
||||
{listOptions.mediaInfo !== false && !enableSideMediaInfo && (
|
||||
<PrimaryMediaInfo
|
||||
className='secondary listItemMediaInfo listItemBodyText'
|
||||
item={item}
|
||||
isEpisodeTitleEnabled={true}
|
||||
isOriginalAirDateEnabled={true}
|
||||
isCaptionIndicatorEnabled={true}
|
||||
getMissingIndicator={getMissingIndicator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!enableContentWrapper && enableOverview && item.Overview && (
|
||||
<Box className='secondary listItem-overview listItemBodyText'>
|
||||
<bdi>{item.Overview}</bdi>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItemBody;
|
30
src/components/listview/List/ListTextWrapper.tsx
Normal file
30
src/components/listview/List/ListTextWrapper.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
interface ListTextWrapperProps {
|
||||
index?: number;
|
||||
isLargeStyle?: boolean;
|
||||
}
|
||||
|
||||
const ListTextWrapper: FC<ListTextWrapperProps> = ({
|
||||
index,
|
||||
isLargeStyle,
|
||||
children
|
||||
}) => {
|
||||
if (index === 0) {
|
||||
if (isLargeStyle) {
|
||||
return (
|
||||
<Typography className='listItemBodyText' variant='h2'>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
return <Box className='listItemBodyText'>{children}</Box>;
|
||||
}
|
||||
} else {
|
||||
return <Box className='secondary listItemBodyText'>{children}</Box>;
|
||||
}
|
||||
};
|
||||
|
||||
export default ListTextWrapper;
|
87
src/components/listview/List/ListViewUserDataButtons.tsx
Normal file
87
src/components/listview/List/ListViewUserDataButtons.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React, { type FC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import itemHelper from '../../itemHelper';
|
||||
import PlayedButton from 'elements/emby-playstatebutton/PlayedButton';
|
||||
import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton';
|
||||
import PlaylistAddIconButton from '../../common/PlaylistAddIconButton';
|
||||
import InfoIconButton from '../../common/InfoIconButton';
|
||||
import RightIconButtons from '../../common/RightIconButtons';
|
||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
interface ListViewUserDataButtonsProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
}
|
||||
|
||||
const ListViewUserDataButtons: FC<ListViewUserDataButtonsProps> = ({
|
||||
item = {},
|
||||
listOptions
|
||||
}) => {
|
||||
const { IsFavorite, Played } = item.UserData ?? {};
|
||||
|
||||
const renderRightButtons = () => {
|
||||
return listOptions.rightButtons?.map((button, index) => (
|
||||
<RightIconButtons
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
className='listItemButton itemAction'
|
||||
id={button.id}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className='listViewUserDataButtons'>
|
||||
{listOptions.addToListButton && (
|
||||
<PlaylistAddIconButton
|
||||
className='paper-icon-button-light listItemButton itemAction'
|
||||
/>
|
||||
|
||||
)}
|
||||
{listOptions.infoButton && (
|
||||
<InfoIconButton
|
||||
className='paper-icon-button-light listItemButton itemAction'
|
||||
/>
|
||||
|
||||
) }
|
||||
|
||||
{listOptions.rightButtons && renderRightButtons()}
|
||||
|
||||
{listOptions.enableUserDataButtons !== false && (
|
||||
<>
|
||||
{itemHelper.canMarkPlayed(item)
|
||||
&& listOptions.enablePlayedButton !== false && (
|
||||
<PlayedButton
|
||||
className='listItemButton'
|
||||
isPlayed={Played}
|
||||
itemId={item.Id}
|
||||
itemType={item.Type}
|
||||
/>
|
||||
)}
|
||||
|
||||
{itemHelper.canRate(item)
|
||||
&& listOptions.enableRatingButton !== false && (
|
||||
<FavoriteButton
|
||||
className='listItemButton'
|
||||
isFavorite={IsFavorite}
|
||||
itemId={item.Id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{listOptions.moreButton !== false && (
|
||||
<MoreVertIconButton
|
||||
className='paper-icon-button-light listItemButton itemAction'
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListViewUserDataButtons;
|
48
src/components/listview/List/ListWrapper.tsx
Normal file
48
src/components/listview/List/ListWrapper.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import layoutManager from '../../layoutManager';
|
||||
import type { DataAttributes } from 'types/dataAttributes';
|
||||
|
||||
interface ListWrapperProps {
|
||||
index: number | undefined;
|
||||
title?: string | null;
|
||||
action?: string | null;
|
||||
dataAttributes?: DataAttributes;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ListWrapper: FC<ListWrapperProps> = ({
|
||||
index,
|
||||
action,
|
||||
title,
|
||||
className,
|
||||
dataAttributes,
|
||||
children
|
||||
}) => {
|
||||
if (layoutManager.tv) {
|
||||
return (
|
||||
<Button
|
||||
data-index={index}
|
||||
className={classNames(
|
||||
className,
|
||||
'itemAction listItem-button listItem-focusscale'
|
||||
)}
|
||||
data-action={action}
|
||||
aria-label={title || ''}
|
||||
{...dataAttributes}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box data-index={index} className={className} {...dataAttributes}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ListWrapper;
|
56
src/components/listview/List/Lists.tsx
Normal file
56
src/components/listview/List/Lists.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React, { type FC } from 'react';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import Box from '@mui/material/Box';
|
||||
import { getIndex } from './listHelper';
|
||||
import ListGroupHeaderWrapper from './ListGroupHeaderWrapper';
|
||||
import List from './List';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
import '../listview.scss';
|
||||
|
||||
interface ListsProps {
|
||||
items: ItemDto[];
|
||||
listOptions?: ListOptions;
|
||||
}
|
||||
|
||||
const Lists: FC<ListsProps> = ({ items = [], listOptions = {} }) => {
|
||||
const groupedData = groupBy(items, (item) => {
|
||||
if (listOptions.showIndex) {
|
||||
return getIndex(item, listOptions);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const renderListItem = (item: ItemDto, index: number) => {
|
||||
return (
|
||||
<List
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${item.Id}-${index}`}
|
||||
index={index}
|
||||
item={item}
|
||||
listOptions={listOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(groupedData).map(
|
||||
([itemGroupTitle, getItems], index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Box key={index}>
|
||||
{itemGroupTitle && (
|
||||
<ListGroupHeaderWrapper index={index}>
|
||||
{itemGroupTitle}
|
||||
</ListGroupHeaderWrapper>
|
||||
)}
|
||||
{getItems.map((item) => renderListItem(item, index))}
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Lists;
|
172
src/components/listview/List/listHelper.ts
Normal file
172
src/components/listview/List/listHelper.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
import { Api } from '@jellyfin/sdk';
|
||||
import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
const sortBySortName = (item: ItemDto): string => {
|
||||
if (item.Type === BaseItemKind.Episode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// SortName
|
||||
const name = (item.SortName ?? item.Name ?? '?')[0].toUpperCase();
|
||||
|
||||
const code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return name.toUpperCase();
|
||||
};
|
||||
|
||||
const sortByOfficialrating = (item: ItemDto): string => {
|
||||
return item.OfficialRating ?? globalize.translate('Unrated');
|
||||
};
|
||||
|
||||
const sortByCommunityRating = (item: ItemDto): string => {
|
||||
if (item.CommunityRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return String(Math.floor(item.CommunityRating));
|
||||
};
|
||||
|
||||
const sortByCriticRating = (item: ItemDto): string => {
|
||||
if (item.CriticRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return String(Math.floor(item.CriticRating));
|
||||
};
|
||||
|
||||
const sortByAlbumArtist = (item: ItemDto): string => {
|
||||
// SortName
|
||||
if (!item.AlbumArtist) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const name = item.AlbumArtist[0].toUpperCase();
|
||||
|
||||
const code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return name.toUpperCase();
|
||||
};
|
||||
|
||||
export function getIndex(item: ItemDto, listOptions: ListOptions): string {
|
||||
if (listOptions.index === 'disc') {
|
||||
return item.ParentIndexNumber == null ?
|
||||
'' :
|
||||
globalize.translate('ValueDiscNumber', item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
const sortBy = (listOptions.sortBy ?? '').toLowerCase();
|
||||
|
||||
if (sortBy.startsWith('sortname')) {
|
||||
return sortBySortName(item);
|
||||
}
|
||||
if (sortBy.startsWith('officialrating')) {
|
||||
return sortByOfficialrating(item);
|
||||
}
|
||||
if (sortBy.startsWith('communityrating')) {
|
||||
return sortByCommunityRating(item);
|
||||
}
|
||||
if (sortBy.startsWith('criticrating')) {
|
||||
return sortByCriticRating(item);
|
||||
}
|
||||
if (sortBy.startsWith('albumartist')) {
|
||||
return sortByAlbumArtist(item);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getImageUrl(
|
||||
item: ItemDto,
|
||||
api: Api | undefined,
|
||||
size: number | undefined
|
||||
) {
|
||||
let imgTag;
|
||||
let itemId;
|
||||
const fillWidth = size;
|
||||
const fillHeight = size;
|
||||
const imgType = ImageType.Primary;
|
||||
|
||||
if (item.ImageTags?.Primary) {
|
||||
imgTag = item.ImageTags.Primary;
|
||||
itemId = item.Id;
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
imgTag = item.AlbumPrimaryImageTag;
|
||||
itemId = item.AlbumId;
|
||||
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
|
||||
imgTag = item.SeriesPrimaryImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (item.ParentPrimaryImageTag) {
|
||||
imgTag = item.ParentPrimaryImageTag;
|
||||
itemId = item.ParentPrimaryImageItemId;
|
||||
}
|
||||
|
||||
if (api && imgTag && imgType && itemId) {
|
||||
const response = getImageApi(api).getItemImageUrlById(itemId, imgType, {
|
||||
fillWidth: fillWidth,
|
||||
fillHeight: fillHeight,
|
||||
tag: imgTag
|
||||
});
|
||||
|
||||
return {
|
||||
imgUrl: response,
|
||||
blurhash: item.ImageBlurHashes?.[imgType]?.[imgTag]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
imgUrl: undefined,
|
||||
blurhash: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function getChannelImageUrl(
|
||||
item: ItemDto,
|
||||
api: Api | undefined,
|
||||
size: number | undefined
|
||||
) {
|
||||
let imgTag;
|
||||
let itemId;
|
||||
const fillWidth = size;
|
||||
const fillHeight = size;
|
||||
const imgType = ImageType.Primary;
|
||||
|
||||
if (item.ChannelId && item.ChannelPrimaryImageTag) {
|
||||
imgTag = item.ChannelPrimaryImageTag;
|
||||
itemId = item.ChannelId;
|
||||
}
|
||||
|
||||
if (api && imgTag && imgType && itemId) {
|
||||
const response = api.getItemImageUrl(itemId, imgType, {
|
||||
fillWidth: fillWidth,
|
||||
fillHeight: fillHeight,
|
||||
tag: imgTag
|
||||
});
|
||||
|
||||
return {
|
||||
imgUrl: response,
|
||||
blurhash: item.ImageBlurHashes?.[imgType]?.[imgTag]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
imgUrl: undefined,
|
||||
blurhash: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function canResume(PlaybackPositionTicks: number | undefined): boolean {
|
||||
return Boolean(
|
||||
PlaybackPositionTicks
|
||||
&& PlaybackPositionTicks > 0
|
||||
);
|
||||
}
|
77
src/components/listview/List/useList.ts
Normal file
77
src/components/listview/List/useList.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import classNames from 'classnames';
|
||||
import { getDataAttributes } from 'utils/items';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
interface UseListProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
}
|
||||
|
||||
function useList({ item, listOptions }: UseListProps) {
|
||||
const action = listOptions.action ?? 'link';
|
||||
const isLargeStyle = listOptions.imageSize === 'large';
|
||||
const enableOverview = listOptions.enableOverview;
|
||||
const clickEntireItem = !!layoutManager.tv;
|
||||
const enableSideMediaInfo = listOptions.enableSideMediaInfo ?? true;
|
||||
const enableContentWrapper =
|
||||
listOptions.enableOverview && !layoutManager.tv;
|
||||
const downloadWidth = isLargeStyle ? 500 : 80;
|
||||
|
||||
const dataAttributes = getDataAttributes(
|
||||
{
|
||||
action,
|
||||
itemServerId: item.ServerId,
|
||||
itemId: item.Id,
|
||||
collectionId: listOptions.collectionId,
|
||||
playlistId: listOptions.playlistId,
|
||||
itemChannelId: item.ChannelId,
|
||||
itemType: item.Type,
|
||||
itemMediaType: item.MediaType,
|
||||
itemCollectionType: item.CollectionType,
|
||||
itemIsFolder: item.IsFolder,
|
||||
itemPlaylistItemId: item.PlaylistItemId
|
||||
}
|
||||
);
|
||||
|
||||
const listWrapperClass = classNames(
|
||||
'listItem',
|
||||
{
|
||||
'listItem-border':
|
||||
listOptions.border
|
||||
?? (listOptions.highlight !== false && !layoutManager.tv)
|
||||
},
|
||||
{ 'itemAction listItem-button': clickEntireItem },
|
||||
{ 'listItem-focusscale': layoutManager.tv },
|
||||
{ 'listItem-largeImage': isLargeStyle },
|
||||
{ 'listItem-withContentWrapper': enableContentWrapper }
|
||||
);
|
||||
|
||||
const getListdWrapperProps = () => ({
|
||||
className: listWrapperClass,
|
||||
title: item.Name,
|
||||
action,
|
||||
dataAttributes
|
||||
});
|
||||
|
||||
const getListContentProps = () => ({
|
||||
item,
|
||||
listOptions,
|
||||
enableContentWrapper,
|
||||
enableOverview,
|
||||
enableSideMediaInfo,
|
||||
clickEntireItem,
|
||||
action,
|
||||
isLargeStyle,
|
||||
downloadWidth
|
||||
});
|
||||
|
||||
return {
|
||||
getListdWrapperProps,
|
||||
getListContentProps
|
||||
};
|
||||
}
|
||||
|
||||
export default useList;
|
167
src/components/listview/List/useListTextlines.tsx
Normal file
167
src/components/listview/List/useListTextlines.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React from 'react';
|
||||
import itemHelper from '../../itemHelper';
|
||||
import datetime from 'scripts/datetime';
|
||||
import ListTextWrapper from './ListTextWrapper';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
function getParentTitle(
|
||||
showParentTitle: boolean | undefined,
|
||||
item: ItemDto,
|
||||
parentTitleWithTitle: boolean | undefined,
|
||||
displayName: string | null | undefined
|
||||
) {
|
||||
let parentTitle = null;
|
||||
if (showParentTitle) {
|
||||
if (item.Type === BaseItemKind.Episode) {
|
||||
parentTitle = item.SeriesName;
|
||||
} else if (item.IsSeries || (item.EpisodeTitle && item.Name)) {
|
||||
parentTitle = item.Name;
|
||||
}
|
||||
}
|
||||
if (showParentTitle && parentTitleWithTitle) {
|
||||
if (displayName) {
|
||||
parentTitle += ' - ';
|
||||
}
|
||||
parentTitle = (parentTitle ?? '') + displayName;
|
||||
}
|
||||
return parentTitle;
|
||||
}
|
||||
|
||||
function getNameOrIndexWithName(
|
||||
item: ItemDto,
|
||||
listOptions: ListOptions,
|
||||
showIndexNumber: boolean | undefined
|
||||
) {
|
||||
let displayName = itemHelper.getDisplayName(item, {
|
||||
includeParentInfo: listOptions.includeParentInfoInTitle
|
||||
});
|
||||
|
||||
if (showIndexNumber && item.IndexNumber != null) {
|
||||
displayName = `${item.IndexNumber}. ${displayName}`;
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
interface UseListTextlinesProps {
|
||||
item: ItemDto;
|
||||
listOptions?: ListOptions;
|
||||
isLargeStyle?: boolean;
|
||||
}
|
||||
|
||||
function useListTextlines({ item = {}, listOptions = {}, isLargeStyle }: UseListTextlinesProps) {
|
||||
const {
|
||||
showProgramDateTime,
|
||||
showProgramTime,
|
||||
showChannel,
|
||||
showParentTitle,
|
||||
showIndexNumber,
|
||||
parentTitleWithTitle,
|
||||
artist
|
||||
} = listOptions;
|
||||
const textLines: string[] = [];
|
||||
|
||||
const addTextLine = (text: string | null) => {
|
||||
if (text) {
|
||||
textLines.push(text);
|
||||
}
|
||||
};
|
||||
|
||||
const addProgramDateTime = () => {
|
||||
if (showProgramDateTime) {
|
||||
const programDateTime = datetime.toLocaleString(
|
||||
datetime.parseISO8601Date(item.StartDate),
|
||||
{
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}
|
||||
);
|
||||
addTextLine(programDateTime);
|
||||
}
|
||||
};
|
||||
|
||||
const addProgramTime = () => {
|
||||
if (showProgramTime) {
|
||||
const programTime = datetime.getDisplayTime(
|
||||
datetime.parseISO8601Date(item.StartDate)
|
||||
);
|
||||
addTextLine(programTime);
|
||||
}
|
||||
};
|
||||
|
||||
const addChannelName = () => {
|
||||
if (showChannel && item.ChannelName) {
|
||||
addTextLine(item.ChannelName);
|
||||
}
|
||||
};
|
||||
|
||||
const displayName = getNameOrIndexWithName(item, listOptions, showIndexNumber);
|
||||
|
||||
const parentTitle = getParentTitle(showParentTitle, item, parentTitleWithTitle, displayName );
|
||||
|
||||
const addParentTitle = () => {
|
||||
addTextLine(parentTitle ?? '');
|
||||
};
|
||||
|
||||
const addDisplayName = () => {
|
||||
if (displayName && !parentTitleWithTitle) {
|
||||
addTextLine(displayName);
|
||||
}
|
||||
};
|
||||
|
||||
const addAlbumArtistOrArtists = () => {
|
||||
if (item.IsFolder && artist !== false) {
|
||||
if (item.AlbumArtist && item.Type === BaseItemKind.MusicAlbum) {
|
||||
addTextLine(item.AlbumArtist);
|
||||
}
|
||||
} else if (artist) {
|
||||
const artistItems = item.ArtistItems;
|
||||
if (artistItems && item.Type !== BaseItemKind.MusicAlbum) {
|
||||
const artists = artistItems.map((a) => a.Name).join(', ');
|
||||
addTextLine(artists);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addCurrentProgram = () => {
|
||||
if (item.Type === BaseItemKind.TvChannel && item.CurrentProgram) {
|
||||
const currentProgram = itemHelper.getDisplayName(
|
||||
item.CurrentProgram
|
||||
);
|
||||
addTextLine(currentProgram);
|
||||
}
|
||||
};
|
||||
|
||||
addProgramDateTime();
|
||||
addProgramTime();
|
||||
addChannelName();
|
||||
addParentTitle();
|
||||
addDisplayName();
|
||||
addAlbumArtistOrArtists();
|
||||
addCurrentProgram();
|
||||
|
||||
const renderTextlines = (text: string, index: number) => {
|
||||
return (
|
||||
<ListTextWrapper
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
index={index}
|
||||
isLargeStyle={isLargeStyle}
|
||||
>
|
||||
<bdi>{text}</bdi>
|
||||
</ListTextWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const listTextLines = textLines?.map((text, index) => renderTextlines(text, index));
|
||||
|
||||
return {
|
||||
listTextLines
|
||||
};
|
||||
}
|
||||
|
||||
export default useListTextlines;
|
|
@ -183,6 +183,7 @@
|
|||
}
|
||||
|
||||
.listItemImage .cardImageIcon {
|
||||
margin: auto;
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
|
|
25
src/components/mediainfo/CaptionMediaInfo.tsx
Normal file
25
src/components/mediainfo/CaptionMediaInfo.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import ClosedCaptionIcon from '@mui/icons-material/ClosedCaption';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface CaptionMediaInfoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CaptionMediaInfo: FC<CaptionMediaInfoProps> = ({ className }) => {
|
||||
const cssClass = classNames(
|
||||
'mediaInfoItem',
|
||||
'mediaInfoText',
|
||||
'closedCaptionMediaInfoText',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={cssClass}>
|
||||
<ClosedCaptionIcon fontSize={'small'} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaptionMediaInfo;
|
25
src/components/mediainfo/CriticRatingMediaInfo.tsx
Normal file
25
src/components/mediainfo/CriticRatingMediaInfo.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface CriticRatingMediaInfoProps {
|
||||
className?: string;
|
||||
criticRating: number;
|
||||
}
|
||||
|
||||
const CriticRatingMediaInfo: FC<CriticRatingMediaInfoProps> = ({
|
||||
className,
|
||||
criticRating
|
||||
}) => {
|
||||
const cssClass = classNames(
|
||||
'mediaInfoCriticRating',
|
||||
'mediaInfoItem',
|
||||
criticRating >= 60 ?
|
||||
'mediaInfoCriticRatingFresh' :
|
||||
'mediaInfoCriticRatingRotten',
|
||||
className
|
||||
);
|
||||
return <Box className={cssClass}>{criticRating}</Box>;
|
||||
};
|
||||
|
||||
export default CriticRatingMediaInfo;
|
31
src/components/mediainfo/EndsAt.tsx
Normal file
31
src/components/mediainfo/EndsAt.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
import datetime from 'scripts/datetime';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface EndsAtProps {
|
||||
className?: string;
|
||||
runTimeTicks: number
|
||||
}
|
||||
|
||||
const EndsAt: FC<EndsAtProps> = ({ runTimeTicks, className }) => {
|
||||
const cssClass = classNames(
|
||||
'mediaInfoItem',
|
||||
'mediaInfoText',
|
||||
'endsAt',
|
||||
className
|
||||
);
|
||||
|
||||
const endTime = new Date().getTime() + (runTimeTicks / 10000);
|
||||
const endDate = new Date(endTime);
|
||||
const displayTime = datetime.getDisplayTime(endDate);
|
||||
|
||||
return (
|
||||
<Box className={cssClass}>
|
||||
{globalize.translate('EndsAtValue', displayTime)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndsAt;
|
27
src/components/mediainfo/MediaInfoItem.tsx
Normal file
27
src/components/mediainfo/MediaInfoItem.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import classNames from 'classnames';
|
||||
import type { MiscInfo } from 'types/mediaInfoItem';
|
||||
|
||||
interface MediaInfoItemProps {
|
||||
className?: string;
|
||||
miscInfo?: MiscInfo ;
|
||||
|
||||
}
|
||||
|
||||
const MediaInfoItem: FC<MediaInfoItemProps> = ({ className, miscInfo }) => {
|
||||
const cssClass = classNames(
|
||||
'mediaInfoItem',
|
||||
'mediaInfoText',
|
||||
className,
|
||||
miscInfo?.cssClass
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={cssClass}>
|
||||
{miscInfo?.text}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaInfoItem;
|
103
src/components/mediainfo/PrimaryMediaInfo.tsx
Normal file
103
src/components/mediainfo/PrimaryMediaInfo.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
import usePrimaryMediaInfo from './usePrimaryMediaInfo';
|
||||
|
||||
import MediaInfoItem from './MediaInfoItem';
|
||||
import StarIcons from './StarIcons';
|
||||
import CaptionMediaInfo from './CaptionMediaInfo';
|
||||
import CriticRatingMediaInfo from './CriticRatingMediaInfo';
|
||||
import EndsAt from './EndsAt';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { MiscInfo } from 'types/mediaInfoItem';
|
||||
|
||||
interface PrimaryMediaInfoProps {
|
||||
className?: string;
|
||||
item: ItemDto;
|
||||
isYearEnabled?: boolean;
|
||||
isContainerEnabled?: boolean;
|
||||
isEpisodeTitleEnabled?: boolean;
|
||||
isCriticRatingEnabled?: boolean;
|
||||
isEndsAtEnabled?: boolean;
|
||||
isOriginalAirDateEnabled?: boolean;
|
||||
isRuntimeEnabled?: boolean;
|
||||
isProgramIndicatorEnabled?: boolean;
|
||||
isEpisodeTitleIndexNumberEnabled?: boolean;
|
||||
isOfficialRatingEnabled?: boolean;
|
||||
isStarRatingEnabled?: boolean;
|
||||
isCaptionIndicatorEnabled?: boolean;
|
||||
isMissingIndicatorEnabled?: boolean;
|
||||
getMissingIndicator: () => React.JSX.Element | null
|
||||
}
|
||||
|
||||
const PrimaryMediaInfo: FC<PrimaryMediaInfoProps> = ({
|
||||
className,
|
||||
item,
|
||||
isYearEnabled = false,
|
||||
isContainerEnabled = false,
|
||||
isEpisodeTitleEnabled = false,
|
||||
isCriticRatingEnabled = false,
|
||||
isEndsAtEnabled = false,
|
||||
isOriginalAirDateEnabled = false,
|
||||
isRuntimeEnabled = false,
|
||||
isProgramIndicatorEnabled = false,
|
||||
isEpisodeTitleIndexNumberEnabled = false,
|
||||
isOfficialRatingEnabled = false,
|
||||
isStarRatingEnabled = false,
|
||||
isCaptionIndicatorEnabled = false,
|
||||
isMissingIndicatorEnabled = false,
|
||||
getMissingIndicator
|
||||
}) => {
|
||||
const miscInfo = usePrimaryMediaInfo({
|
||||
item,
|
||||
isYearEnabled,
|
||||
isContainerEnabled,
|
||||
isEpisodeTitleEnabled,
|
||||
isOriginalAirDateEnabled,
|
||||
isRuntimeEnabled,
|
||||
isProgramIndicatorEnabled,
|
||||
isEpisodeTitleIndexNumberEnabled,
|
||||
isOfficialRatingEnabled
|
||||
});
|
||||
const {
|
||||
StartDate,
|
||||
HasSubtitles,
|
||||
MediaType,
|
||||
RunTimeTicks,
|
||||
CommunityRating,
|
||||
CriticRating
|
||||
} = item;
|
||||
|
||||
const cssClass = classNames(className);
|
||||
|
||||
const renderMediaInfo = (info: MiscInfo | undefined, index: number) => (
|
||||
<MediaInfoItem key={index} miscInfo={info} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={cssClass}>
|
||||
{miscInfo.map((info, index) => renderMediaInfo(info, index))}
|
||||
|
||||
{isStarRatingEnabled && CommunityRating && (
|
||||
<StarIcons communityRating={CommunityRating} />
|
||||
)}
|
||||
|
||||
{HasSubtitles && isCaptionIndicatorEnabled && <CaptionMediaInfo />}
|
||||
|
||||
{CriticRating && isCriticRatingEnabled && (
|
||||
<CriticRatingMediaInfo criticRating={CriticRating} />
|
||||
)}
|
||||
|
||||
{isEndsAtEnabled
|
||||
&& MediaType === 'Video'
|
||||
&& RunTimeTicks
|
||||
&& !StartDate && <EndsAt runTimeTicks={RunTimeTicks} />}
|
||||
|
||||
{isMissingIndicatorEnabled && (
|
||||
getMissingIndicator()
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrimaryMediaInfo;
|
31
src/components/mediainfo/StarIcons.tsx
Normal file
31
src/components/mediainfo/StarIcons.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
interface StarIconsProps {
|
||||
className?: string;
|
||||
communityRating: number;
|
||||
}
|
||||
|
||||
const StarIcons: FC<StarIconsProps> = ({ className, communityRating }) => {
|
||||
const theme = useTheme();
|
||||
const cssClass = classNames(
|
||||
'mediaInfoItem',
|
||||
'mediaInfoText',
|
||||
'starRatingContainer',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={cssClass}>
|
||||
<StarIcon fontSize={'small'} sx={{
|
||||
color: theme.palette.starIcon.main
|
||||
}} />
|
||||
{communityRating.toFixed(1)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarIcons;
|
523
src/components/mediainfo/usePrimaryMediaInfo.tsx
Normal file
523
src/components/mediainfo/usePrimaryMediaInfo.tsx
Normal file
|
@ -0,0 +1,523 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import datetime from 'scripts/datetime';
|
||||
import globalize from 'scripts/globalize';
|
||||
import itemHelper from '../itemHelper';
|
||||
import type { NullableNumber, NullableString } from 'types/base/common/shared/types';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { MiscInfo } from 'types/mediaInfoItem';
|
||||
|
||||
function shouldShowFolderRuntime(
|
||||
itemType: NullableString,
|
||||
itemMediaType: NullableString
|
||||
): boolean {
|
||||
return (
|
||||
itemType === BaseItemKind.MusicAlbum
|
||||
|| itemMediaType === 'MusicArtist'
|
||||
|| itemType === BaseItemKind.Playlist
|
||||
|| itemMediaType === 'Playlist'
|
||||
|| itemMediaType === 'MusicGenre'
|
||||
);
|
||||
}
|
||||
|
||||
function addTrackCountOrItemCount(
|
||||
showFolderRuntime: boolean,
|
||||
itemSongCount: NullableNumber,
|
||||
itemChildCount: NullableNumber,
|
||||
itemRunTimeTicks: NullableNumber,
|
||||
itemType: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (showFolderRuntime) {
|
||||
const count = itemSongCount ?? itemChildCount;
|
||||
if (count) {
|
||||
addMiscInfo({ text: globalize.translate('TrackCount', count) });
|
||||
}
|
||||
|
||||
if (itemRunTimeTicks) {
|
||||
addMiscInfo({ text: datetime.getDisplayDuration(itemRunTimeTicks) });
|
||||
}
|
||||
} else if (itemType === BaseItemKind.PhotoAlbum || itemType === BaseItemKind.BoxSet) {
|
||||
const count = itemChildCount;
|
||||
if (count) {
|
||||
addMiscInfo({ text: globalize.translate('ItemCount', count) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addOriginalAirDateInfo(
|
||||
itemType: NullableString,
|
||||
itemMediaType: NullableString,
|
||||
isOriginalAirDateEnabled: boolean,
|
||||
itemPremiereDate: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
itemPremiereDate
|
||||
&& (itemType === BaseItemKind.Episode || itemMediaType === 'Photo')
|
||||
&& isOriginalAirDateEnabled
|
||||
) {
|
||||
try {
|
||||
//don't modify date to locale if episode. Only Dates (not times) are stored, or editable in the edit metadata dialog
|
||||
const date = datetime.parseISO8601Date(
|
||||
itemPremiereDate,
|
||||
itemType !== BaseItemKind.Episode
|
||||
);
|
||||
addMiscInfo({ text: datetime.toLocaleDateString(date) });
|
||||
} catch (e) {
|
||||
console.error('error parsing date:', itemPremiereDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSeriesTimerInfo(
|
||||
itemType: NullableString,
|
||||
itemRecordAnyTime: boolean | undefined,
|
||||
itemStartDate: NullableString,
|
||||
itemRecordAnyChannel: boolean | undefined,
|
||||
itemChannelName: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (itemType === 'SeriesTimer') {
|
||||
if (itemRecordAnyTime) {
|
||||
addMiscInfo({ text: globalize.translate('Anytime') });
|
||||
} else {
|
||||
addMiscInfo({ text: datetime.getDisplayTime(itemStartDate) });
|
||||
}
|
||||
|
||||
if (itemRecordAnyChannel) {
|
||||
addMiscInfo({ text: globalize.translate('AllChannels') });
|
||||
} else {
|
||||
addMiscInfo({
|
||||
text: itemChannelName ?? globalize.translate('OneChannel')
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addProgramIndicatorInfo(
|
||||
program: ItemDto | undefined,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
program?.IsLive
|
||||
&& userSettings.get('guide-indicator-live', false) === 'true'
|
||||
) {
|
||||
addMiscInfo({
|
||||
text: globalize.translate('Live'),
|
||||
cssClass: 'mediaInfoProgramAttribute liveTvProgram'
|
||||
});
|
||||
} else if (
|
||||
program?.IsPremiere
|
||||
&& userSettings.get('guide-indicator-premiere', false) === 'true'
|
||||
) {
|
||||
addMiscInfo({
|
||||
text: globalize.translate('Premiere'),
|
||||
cssClass: 'mediaInfoProgramAttribute premiereTvProgram'
|
||||
});
|
||||
} else if (
|
||||
program?.IsSeries
|
||||
&& !program?.IsRepeat
|
||||
&& userSettings.get('guide-indicator-new', false) === 'true'
|
||||
) {
|
||||
addMiscInfo({
|
||||
text: globalize.translate('New'),
|
||||
cssClass: 'mediaInfoProgramAttribute newTvProgram'
|
||||
});
|
||||
} else if (
|
||||
program?.IsSeries
|
||||
&& program?.IsRepeat
|
||||
&& userSettings.get('guide-indicator-repeat', false) === 'true'
|
||||
) {
|
||||
addMiscInfo({
|
||||
text: globalize.translate('Repeat'),
|
||||
cssClass: 'mediaInfoProgramAttribute repeatTvProgram'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addProgramIndicators(
|
||||
item: ItemDto,
|
||||
isYearEnabled: boolean,
|
||||
isEpisodeTitleEnabled: boolean,
|
||||
isOriginalAirDateEnabled: boolean,
|
||||
isProgramIndicatorEnabled: boolean,
|
||||
isEpisodeTitleIndexNumberEnabled: boolean,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (item.Type === BaseItemKind.Program || item.Type === 'Timer') {
|
||||
let program = item;
|
||||
if (item.Type === 'Timer' && item.ProgramInfo) {
|
||||
program = item.ProgramInfo;
|
||||
}
|
||||
|
||||
if (isProgramIndicatorEnabled !== false) {
|
||||
addProgramIndicatorInfo(program, addMiscInfo);
|
||||
}
|
||||
|
||||
addProgramTextInfo(
|
||||
program,
|
||||
isEpisodeTitleEnabled,
|
||||
isEpisodeTitleIndexNumberEnabled,
|
||||
isOriginalAirDateEnabled,
|
||||
isYearEnabled,
|
||||
addMiscInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addProgramTextInfo(
|
||||
program: ItemDto,
|
||||
isEpisodeTitleEnabled: boolean,
|
||||
isEpisodeTitleIndexNumberEnabled: boolean,
|
||||
isOriginalAirDateEnabled: boolean,
|
||||
isYearEnabled: boolean,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if ((program?.IsSeries || program?.EpisodeTitle)
|
||||
&& isEpisodeTitleEnabled !== false) {
|
||||
const text = itemHelper.getDisplayName(program, {
|
||||
includeIndexNumber: isEpisodeTitleIndexNumberEnabled
|
||||
});
|
||||
|
||||
if (text) {
|
||||
addMiscInfo({ text: text });
|
||||
}
|
||||
} else if (
|
||||
program?.ProductionYear
|
||||
&& ((program?.IsMovie && isOriginalAirDateEnabled !== false)
|
||||
|| isYearEnabled !== false)
|
||||
) {
|
||||
addMiscInfo({ text: program.ProductionYear });
|
||||
} else if (program?.PremiereDate && isOriginalAirDateEnabled !== false) {
|
||||
try {
|
||||
const date = datetime.parseISO8601Date(program.PremiereDate);
|
||||
const text = globalize.translate(
|
||||
'OriginalAirDateValue',
|
||||
datetime.toLocaleDateString(date)
|
||||
);
|
||||
addMiscInfo({ text: text });
|
||||
} catch (e) {
|
||||
console.error('error parsing date:', program.PremiereDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addStartDateInfo(
|
||||
itemStartDate: NullableString,
|
||||
itemType: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
itemStartDate
|
||||
&& itemType !== BaseItemKind.Program
|
||||
&& itemType !== 'SeriesTimer'
|
||||
&& itemType !== 'Timer'
|
||||
) {
|
||||
try {
|
||||
const date = datetime.parseISO8601Date(itemStartDate);
|
||||
addMiscInfo({ text: datetime.toLocaleDateString(date) });
|
||||
|
||||
if (itemType !== BaseItemKind.Recording) {
|
||||
addMiscInfo({ text: datetime.getDisplayTime(date) });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error parsing date:', itemStartDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSeriesProductionYearInfo(
|
||||
itemProductionYear: NullableNumber,
|
||||
itemType: NullableString,
|
||||
isYearEnabled: boolean,
|
||||
itemStatus: NullableString,
|
||||
itemEndDate: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (itemProductionYear && isYearEnabled && itemType === BaseItemKind.Series) {
|
||||
if (itemStatus === 'Continuing') {
|
||||
addMiscInfo({
|
||||
text: globalize.translate(
|
||||
'SeriesYearToPresent',
|
||||
datetime.toLocaleString(itemProductionYear, {
|
||||
useGrouping: false
|
||||
})
|
||||
)
|
||||
});
|
||||
} else {
|
||||
addproductionYearWithEndDate(itemProductionYear, itemEndDate, addMiscInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addproductionYearWithEndDate(
|
||||
itemProductionYear: number,
|
||||
itemEndDate: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
let productionYear = datetime.toLocaleString(itemProductionYear, {
|
||||
useGrouping: false
|
||||
});
|
||||
|
||||
if (itemEndDate) {
|
||||
try {
|
||||
const endYear = datetime.toLocaleString(
|
||||
datetime.parseISO8601Date(itemEndDate).getFullYear(),
|
||||
{ useGrouping: false }
|
||||
);
|
||||
/* At this point, text will contain only the start year */
|
||||
if (endYear !== itemProductionYear) {
|
||||
productionYear += `-${endYear}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error parsing date:', itemEndDate);
|
||||
}
|
||||
}
|
||||
addMiscInfo({ text: productionYear });
|
||||
}
|
||||
|
||||
function addYearInfo(
|
||||
isYearEnabled: boolean,
|
||||
itemType: NullableString,
|
||||
itemMediaType: NullableString,
|
||||
itemProductionYear: NullableNumber,
|
||||
itemPremiereDate: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
isYearEnabled
|
||||
&& itemType !== BaseItemKind.Series
|
||||
&& itemType !== BaseItemKind.Episode
|
||||
&& itemType !== BaseItemKind.Person
|
||||
&& itemMediaType !== 'Photo'
|
||||
&& itemType !== BaseItemKind.Program
|
||||
&& itemType !== BaseItemKind.Season
|
||||
) {
|
||||
if (itemProductionYear) {
|
||||
addMiscInfo({ text: itemProductionYear });
|
||||
} else if (itemPremiereDate) {
|
||||
try {
|
||||
const text = datetime.toLocaleString(
|
||||
datetime.parseISO8601Date(itemPremiereDate).getFullYear(),
|
||||
{ useGrouping: false }
|
||||
);
|
||||
addMiscInfo({ text: text });
|
||||
} catch (e) {
|
||||
console.error('error parsing date:', itemPremiereDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addVideo3DFormat(
|
||||
itemVideo3DFormat: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (itemVideo3DFormat) {
|
||||
addMiscInfo({ text: '3D' });
|
||||
}
|
||||
}
|
||||
|
||||
function addRunTimeInfo(
|
||||
itemRunTimeTicks: NullableNumber,
|
||||
itemType: NullableString,
|
||||
showFolderRuntime: boolean,
|
||||
isRuntimeEnabled: boolean,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
itemRunTimeTicks
|
||||
&& itemType !== BaseItemKind.Series
|
||||
&& itemType !== BaseItemKind.Program
|
||||
&& itemType !== 'Timer'
|
||||
&& itemType !== BaseItemKind.Book
|
||||
&& !showFolderRuntime
|
||||
&& isRuntimeEnabled
|
||||
) {
|
||||
if (itemType === BaseItemKind.Audio) {
|
||||
addMiscInfo({ text: datetime.getDisplayRunningTime(itemRunTimeTicks) });
|
||||
} else {
|
||||
addMiscInfo({ text: datetime.getDisplayDuration(itemRunTimeTicks) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addOfficialRatingInfo(
|
||||
itemOfficialRating: NullableString,
|
||||
itemType: NullableString,
|
||||
isOfficialRatingEnabled: boolean,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (
|
||||
itemOfficialRating
|
||||
&& isOfficialRatingEnabled
|
||||
&& itemType !== BaseItemKind.Season
|
||||
&& itemType !== BaseItemKind.Episode
|
||||
) {
|
||||
addMiscInfo({
|
||||
text: itemOfficialRating,
|
||||
cssClass: 'mediaInfoOfficialRating'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addAudioContainer(
|
||||
itemContainer: NullableString,
|
||||
isContainerEnabled: boolean,
|
||||
itemType: NullableString,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (itemContainer && isContainerEnabled && itemType === BaseItemKind.Audio) {
|
||||
addMiscInfo({ text: itemContainer });
|
||||
}
|
||||
}
|
||||
|
||||
function addPhotoSize(
|
||||
itemMediaType: NullableString,
|
||||
itemWidth: NullableNumber,
|
||||
itemHeight: NullableNumber,
|
||||
addMiscInfo: (val: MiscInfo) => void
|
||||
): void {
|
||||
if (itemMediaType === 'Photo' && itemWidth && itemHeight) {
|
||||
const size = `${itemWidth}x${itemHeight}`;
|
||||
|
||||
addMiscInfo({ text: size });
|
||||
}
|
||||
}
|
||||
|
||||
interface UsePrimaryMediaInfoProps {
|
||||
item: ItemDto;
|
||||
isYearEnabled: boolean;
|
||||
isContainerEnabled: boolean;
|
||||
isEpisodeTitleEnabled: boolean;
|
||||
isOriginalAirDateEnabled: boolean;
|
||||
isRuntimeEnabled: boolean;
|
||||
isProgramIndicatorEnabled: boolean;
|
||||
isEpisodeTitleIndexNumberEnabled: boolean;
|
||||
isOfficialRatingEnabled: boolean;
|
||||
}
|
||||
|
||||
function usePrimaryMediaInfo({
|
||||
item,
|
||||
isYearEnabled = false,
|
||||
isContainerEnabled = false,
|
||||
isEpisodeTitleEnabled = false,
|
||||
isOriginalAirDateEnabled = false,
|
||||
isRuntimeEnabled = false,
|
||||
isProgramIndicatorEnabled = false,
|
||||
isEpisodeTitleIndexNumberEnabled = false,
|
||||
isOfficialRatingEnabled = false
|
||||
}: UsePrimaryMediaInfoProps) {
|
||||
const {
|
||||
EndDate,
|
||||
Status,
|
||||
StartDate,
|
||||
ProductionYear,
|
||||
Video3DFormat,
|
||||
Type,
|
||||
Width,
|
||||
Height,
|
||||
MediaType,
|
||||
SongCount,
|
||||
RecordAnyTime,
|
||||
RecordAnyChannel,
|
||||
ChannelName,
|
||||
ChildCount,
|
||||
RunTimeTicks,
|
||||
PremiereDate,
|
||||
OfficialRating,
|
||||
Container
|
||||
} = item;
|
||||
|
||||
const miscInfo: MiscInfo[] = [];
|
||||
|
||||
const addMiscInfo = (val: MiscInfo) => {
|
||||
if (val) {
|
||||
miscInfo.push(val);
|
||||
}
|
||||
};
|
||||
|
||||
const showFolderRuntime = shouldShowFolderRuntime(Type, MediaType);
|
||||
|
||||
addTrackCountOrItemCount(
|
||||
showFolderRuntime,
|
||||
SongCount,
|
||||
ChildCount,
|
||||
RunTimeTicks,
|
||||
Type,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addOriginalAirDateInfo(
|
||||
Type,
|
||||
MediaType,
|
||||
isOriginalAirDateEnabled,
|
||||
PremiereDate,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addSeriesTimerInfo(
|
||||
Type,
|
||||
RecordAnyTime,
|
||||
StartDate,
|
||||
RecordAnyChannel,
|
||||
ChannelName,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addStartDateInfo(StartDate, Type, addMiscInfo);
|
||||
|
||||
addSeriesProductionYearInfo(
|
||||
ProductionYear,
|
||||
Type,
|
||||
isYearEnabled,
|
||||
Status,
|
||||
EndDate,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addProgramIndicators(
|
||||
item,
|
||||
isProgramIndicatorEnabled,
|
||||
isEpisodeTitleEnabled,
|
||||
isEpisodeTitleIndexNumberEnabled,
|
||||
isOriginalAirDateEnabled,
|
||||
isYearEnabled,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addYearInfo(
|
||||
isYearEnabled,
|
||||
Type,
|
||||
MediaType,
|
||||
ProductionYear,
|
||||
PremiereDate,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addRunTimeInfo(
|
||||
RunTimeTicks,
|
||||
Type,
|
||||
showFolderRuntime,
|
||||
isRuntimeEnabled,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addOfficialRatingInfo(
|
||||
OfficialRating,
|
||||
Type,
|
||||
isOfficialRatingEnabled,
|
||||
addMiscInfo
|
||||
);
|
||||
|
||||
addVideo3DFormat(Video3DFormat, addMiscInfo);
|
||||
|
||||
addPhotoSize(MediaType, Width, Height, addMiscInfo);
|
||||
|
||||
addAudioContainer(Container, isContainerEnabled, Type, addMiscInfo);
|
||||
|
||||
return miscInfo;
|
||||
}
|
||||
|
||||
export default usePrimaryMediaInfo;
|
Loading…
Add table
Add a link
Reference in a new issue