mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Convert CardView to react
This commit is contained in:
parent
9efc71fa3b
commit
97472ac8bb
20 changed files with 1993 additions and 11 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, { 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/itemDto';
|
||||||
|
|
||||||
|
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;
|
79
src/components/cardbuilder/Card/CardBox.tsx
Normal file
79
src/components/cardbuilder/Card/CardBox.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
|
||||||
|
import React, { 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 type { ItemDto } from 'types/itemDto';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
|
||||||
|
interface CardBoxProps {
|
||||||
|
item: ItemDto;
|
||||||
|
cardOptions: CardOptions;
|
||||||
|
className: string;
|
||||||
|
shape: string | null | 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, { FC } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { getDefaultBackgroundClass } from '../cardBuilderUtils';
|
||||||
|
import CardImageContainer from './CardImageContainer';
|
||||||
|
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
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, { 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/itemDto';
|
||||||
|
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;
|
83
src/components/cardbuilder/Card/CardHoverMenu.tsx
Normal file
83
src/components/cardbuilder/Card/CardHoverMenu.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
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/itemDto';
|
||||||
|
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={escapeHTML(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;
|
82
src/components/cardbuilder/Card/CardImageContainer.tsx
Normal file
82
src/components/cardbuilder/Card/CardImageContainer.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import React, { 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/itemDto';
|
||||||
|
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 === '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 === '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, { FC } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import CardFooterText from './CardFooterText';
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
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, { 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/itemDto';
|
||||||
|
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;
|
96
src/components/cardbuilder/Card/CardOverlayButtons.tsx
Normal file
96
src/components/cardbuilder/Card/CardOverlayButtons.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import PlayArrowIconButton from '../../common/PlayArrowIconButton';
|
||||||
|
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||||
|
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
|
||||||
|
const sholudShowOverlayPlayButton = (
|
||||||
|
overlayPlayButton: boolean | undefined,
|
||||||
|
item: ItemDto
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
overlayPlayButton
|
||||||
|
&& !item.IsPlaceHolder
|
||||||
|
&& (item.LocationType !== 'Virtual'
|
||||||
|
|| !item.MediaType
|
||||||
|
|| item.Type === 'Program')
|
||||||
|
&& item.Type !== '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 btnCssClass = classNames(
|
||||||
|
'paper-icon-button-light',
|
||||||
|
'cardOverlayButton',
|
||||||
|
'itemAction'
|
||||||
|
);
|
||||||
|
|
||||||
|
const centerPlayButtonClass = classNames(
|
||||||
|
btnCssClass,
|
||||||
|
'cardOverlayButton-centered'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardOverlayButtons;
|
33
src/components/cardbuilder/Card/CardText.tsx
Normal file
33
src/components/cardbuilder/Card/CardText.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{escapeHTML(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, { 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;
|
32
src/components/cardbuilder/Card/Cards.tsx
Normal file
32
src/components/cardbuilder/Card/Cards.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { setCardData } from '../cardBuilder';
|
||||||
|
import Card from './Card';
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
import '../card.scss';
|
||||||
|
|
||||||
|
interface CardsProps {
|
||||||
|
items: ItemDto[];
|
||||||
|
cardOptions: CardOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cards: FC<CardsProps> = ({
|
||||||
|
items = [],
|
||||||
|
cardOptions
|
||||||
|
}) => {
|
||||||
|
setCardData(items, cardOptions);
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
<>
|
||||||
|
{items?.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.Id}
|
||||||
|
item ={item}
|
||||||
|
cardOptions= {cardOptions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cards;
|
721
src/components/cardbuilder/Card/cardHelper.ts
Normal file
721
src/components/cardbuilder/Card/cardHelper.ts
Normal file
|
@ -0,0 +1,721 @@
|
||||||
|
import {
|
||||||
|
BaseItemDto,
|
||||||
|
BaseItemKind,
|
||||||
|
BaseItemPerson,
|
||||||
|
ImageType
|
||||||
|
} from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { Api } from '@jellyfin/sdk';
|
||||||
|
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 { ItemDto, NullableNumber, NullableString } from 'types/itemDto';
|
||||||
|
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 = api.getItemImageUrl(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 && 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
|
||||||
|
};
|
||||||
|
}
|
121
src/components/cardbuilder/Card/useCard.ts
Normal file
121
src/components/cardbuilder/Card/useCard.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import useCardImageUrl from './useCardImageUrl';
|
||||||
|
import {
|
||||||
|
resolveAction,
|
||||||
|
resolveMixedShapeByAspectRatio
|
||||||
|
} from '../cardBuilderUtils';
|
||||||
|
import { getDataAttributes } from 'utils/items';
|
||||||
|
import layoutManager from 'components/layoutManager';
|
||||||
|
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
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 === '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 !== 'MusicAlbum'
|
||||||
|
&& item.Type !== 'MusicArtist'
|
||||||
|
&& item.Type !== '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;
|
301
src/components/cardbuilder/Card/useCardImageUrl.ts
Normal file
301
src/components/cardbuilder/Card/useCardImageUrl.ts
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { getDesiredAspect } from '../cardBuilderUtils';
|
||||||
|
|
||||||
|
import type { ItemDto } from 'types/itemDto';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
|
||||||
|
type ShapeType = string | null | undefined;
|
||||||
|
|
||||||
|
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(
|
||||||
|
width: number | undefined,
|
||||||
|
primaryImageAspectRatio: number | null | undefined
|
||||||
|
) {
|
||||||
|
if (width && primaryImageAspectRatio) {
|
||||||
|
return Math.round(width / primaryImageAspectRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isForceName(cardOptions: CardOptions) {
|
||||||
|
return !!(cardOptions.preferThumb && cardOptions.showTitle !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCoverImage(
|
||||||
|
primaryImageAspectRatio: number | null | undefined,
|
||||||
|
uiAspect: number | null
|
||||||
|
) {
|
||||||
|
if (primaryImageAspectRatio && uiAspect) {
|
||||||
|
return Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect <= 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowPreferBanner(
|
||||||
|
imageTagsBanner: string | undefined,
|
||||||
|
cardOptions: CardOptions,
|
||||||
|
shape: ShapeType
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
(cardOptions.preferBanner || shape === '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(
|
||||||
|
item: ItemDto,
|
||||||
|
cardOptions: CardOptions
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(item.SeriesThumbImageTag) && cardOptions.inheritThumb !== false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowParentThumbImageTag(
|
||||||
|
item: ItemDto,
|
||||||
|
cardOptions: CardOptions
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(item.ParentThumbItemId) && cardOptions.inheritThumb !== false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowParentBackdropImageTags(item: ItemDto): boolean {
|
||||||
|
return Boolean(item.AlbumId) && Boolean(item.AlbumPrimaryImageTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowPreferThumb(type: string | null | undefined, cardOptions: CardOptions): boolean {
|
||||||
|
return Boolean(cardOptions.preferThumb) && !(type === BaseItemKind.Program || type === BaseItemKind.Episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardImageInfo(
|
||||||
|
item: ItemDto,
|
||||||
|
cardOptions: CardOptions,
|
||||||
|
shape: ShapeType
|
||||||
|
) {
|
||||||
|
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 (item.ImageTags?.Thumb) {
|
||||||
|
imgType = ImageType.Thumb;
|
||||||
|
imgTag = item.ImageTags.Thumb;
|
||||||
|
itemId = item.Id;*/
|
||||||
|
} else if (shouldShowSeriesThumbImageTag(item, cardOptions)) {
|
||||||
|
imgType = ImageType.Thumb;
|
||||||
|
imgTag = item.SeriesThumbImageTag;
|
||||||
|
itemId = item.SeriesId;
|
||||||
|
} else if (shouldShowParentThumbImageTag(item, 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: ShapeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = api?.getItemImageUrl(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/itemDto';
|
||||||
|
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;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardDefaultText {
|
.cardImageContainer > .cardDefaultText {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
|
@ -408,6 +408,7 @@ button::-moz-focus-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
[dir="ltr"] & {
|
[dir="ltr"] & {
|
||||||
right: 0.225em;
|
right: 0.225em;
|
||||||
|
@ -852,7 +853,7 @@ button::-moz-focus-inner {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardOverlayFab-primary {
|
.cardOverlayContainer > .cardOverlayFab-primary {
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -865,7 +866,7 @@ button::-moz-focus-inner {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardOverlayFab-primary:hover {
|
.cardOverlayContainer > .cardOverlayFab-primary:hover {
|
||||||
transform: scale(1.4, 1.4);
|
transform: scale(1.4, 1.4);
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
||||||
* @param {Object} items - A set of items.
|
* @param {Object} items - A set of items.
|
||||||
* @param {Object} options - Options for handling the items.
|
* @param {Object} options - Options for handling the items.
|
||||||
*/
|
*/
|
||||||
function setCardData(items, options) {
|
export function setCardData(items, options) {
|
||||||
options.shape = options.shape || 'auto';
|
options.shape = options.shape || 'auto';
|
||||||
|
|
||||||
const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items);
|
const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items);
|
||||||
|
|
|
@ -10,10 +10,10 @@ const ASPECT_RATIOS = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the item is live TV.
|
* 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.
|
* @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
|
* Resolves Card action to display
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDtoImageBlurHashes, BaseItemKind, ImageType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import type { ItemDto, NullableString } from './itemDto';
|
||||||
|
import { ParentId } from './library';
|
||||||
|
|
||||||
export interface CardOptions {
|
export interface CardOptions {
|
||||||
itemsContainer?: HTMLElement | null;
|
itemsContainer?: HTMLElement | null;
|
||||||
parentContainer?: HTMLElement | null;
|
parentContainer?: HTMLElement | null;
|
||||||
items?: BaseItemDto[] | null;
|
items?: ItemDto[] | null;
|
||||||
allowBottomPadding?: boolean;
|
allowBottomPadding?: boolean;
|
||||||
centerText?: boolean;
|
centerText?: boolean;
|
||||||
coverImage?: boolean;
|
coverImage?: boolean;
|
||||||
|
@ -12,13 +14,15 @@ export interface CardOptions {
|
||||||
overlayMoreButton?: boolean;
|
overlayMoreButton?: boolean;
|
||||||
overlayPlayButton?: boolean;
|
overlayPlayButton?: boolean;
|
||||||
overlayText?: boolean;
|
overlayText?: boolean;
|
||||||
|
imageBlurhashes?: BaseItemDtoImageBlurHashes | null;
|
||||||
|
preferBanner?: boolean;
|
||||||
preferThumb?: boolean | string | null;
|
preferThumb?: boolean | string | null;
|
||||||
preferDisc?: boolean;
|
preferDisc?: boolean;
|
||||||
preferLogo?: boolean;
|
preferLogo?: boolean;
|
||||||
scalable?: boolean;
|
scalable?: boolean;
|
||||||
shape?: string | null;
|
shape?: string | null;
|
||||||
lazy?: boolean;
|
lazy?: boolean;
|
||||||
cardLayout?: boolean | string;
|
cardLayout?: boolean | null;
|
||||||
showParentTitle?: boolean;
|
showParentTitle?: boolean;
|
||||||
showParentTitleOrTitle?: boolean;
|
showParentTitleOrTitle?: boolean;
|
||||||
showAirTime?: boolean;
|
showAirTime?: boolean;
|
||||||
|
@ -37,7 +41,7 @@ export interface CardOptions {
|
||||||
action?: string | null;
|
action?: string | null;
|
||||||
defaultShape?: string;
|
defaultShape?: string;
|
||||||
indexBy?: string;
|
indexBy?: string;
|
||||||
parentId?: string | null;
|
parentId?: ParentId;
|
||||||
showMenu?: boolean;
|
showMenu?: boolean;
|
||||||
cardCssClass?: string | null;
|
cardCssClass?: string | null;
|
||||||
cardClass?: string | null;
|
cardClass?: string | null;
|
||||||
|
@ -61,9 +65,10 @@ export interface CardOptions {
|
||||||
showSeriesTimerChannel?: boolean;
|
showSeriesTimerChannel?: boolean;
|
||||||
showSongCount?: boolean;
|
showSongCount?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
widths?: any;
|
||||||
showChannelLogo?: boolean;
|
showChannelLogo?: boolean;
|
||||||
showLogo?: boolean;
|
showLogo?: boolean;
|
||||||
serverId?: string;
|
serverId?: NullableString;
|
||||||
collectionId?: string | null;
|
collectionId?: string | null;
|
||||||
playlistId?: string | null;
|
playlistId?: string | null;
|
||||||
defaultCardImageIcon?: string;
|
defaultCardImageIcon?: string;
|
||||||
|
@ -72,4 +77,46 @@ export interface CardOptions {
|
||||||
showGroupCount?: boolean;
|
showGroupCount?: boolean;
|
||||||
containerClass?: string;
|
containerClass?: string;
|
||||||
noItemsMessage?: string;
|
noItemsMessage?: string;
|
||||||
|
showIndex?: boolean;
|
||||||
|
index?: string;
|
||||||
|
showIndexNumber?: boolean;
|
||||||
|
enableContentWrapper?: boolean;
|
||||||
|
enableOverview?: boolean;
|
||||||
|
enablePlayedButton?: boolean;
|
||||||
|
infoButton?: boolean;
|
||||||
|
imageSize?: string;
|
||||||
|
enableSideMediaInfo?: boolean;
|
||||||
|
imagePlayButton?: boolean;
|
||||||
|
border?: boolean;
|
||||||
|
highlight?: boolean;
|
||||||
|
smallIcon?: boolean;
|
||||||
|
artist?: boolean;
|
||||||
|
addToListButton?: boolean;
|
||||||
|
enableUserDataButtons?: boolean;
|
||||||
|
enableRatingButton?: boolean;
|
||||||
|
image?: boolean;
|
||||||
|
imageSource?: string;
|
||||||
|
showProgramDateTime?: boolean;
|
||||||
|
showChannel?: boolean;
|
||||||
|
mediaInfo?: boolean;
|
||||||
|
moreButton?: boolean;
|
||||||
|
recordButton?: boolean;
|
||||||
|
dragHandle?: boolean;
|
||||||
|
showProgramTime?: boolean;
|
||||||
|
parentTitleWithTitle?: boolean;
|
||||||
|
showIndexNumberLeft?: boolean;
|
||||||
|
sortBy?: string;
|
||||||
|
textLines?: (item: ItemDto) => (BaseItemKind | string | undefined)[];
|
||||||
|
userData?: UserItemDataDto;
|
||||||
|
rightButtons?: {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
uiAspect?: number | null;
|
||||||
|
primaryImageAspectRatio?: number | null;
|
||||||
|
rows?: number | null;
|
||||||
|
imageType?: ImageType;
|
||||||
|
queryKey?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue