1
0
Fork 0
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:
grafixeyehero 2024-01-31 04:25:14 +03:00
parent 9efc71fa3b
commit 97472ac8bb
20 changed files with 1993 additions and 11 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 || '' || '&nbsp;' };
}
}
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
};
}

View 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;

View 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;

View 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}>
&nbsp;
</Box>
);
valid++;
}
}
return components;
};
const cardTextLines = renderCardTextLines();
return {
cardTextLines
};
}
export default useCardText;

View file

@ -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;
}

View file

@ -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);

View file

@ -10,10 +10,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

View file

@ -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 type { ItemDto, NullableString } from './itemDto';
import { ParentId } from './library';
export interface CardOptions {
itemsContainer?: HTMLElement | null;
parentContainer?: HTMLElement | null;
items?: BaseItemDto[] | null;
items?: ItemDto[] | null;
allowBottomPadding?: boolean;
centerText?: boolean;
coverImage?: boolean;
@ -12,13 +14,15 @@ export interface CardOptions {
overlayMoreButton?: boolean;
overlayPlayButton?: boolean;
overlayText?: boolean;
imageBlurhashes?: BaseItemDtoImageBlurHashes | null;
preferBanner?: boolean;
preferThumb?: boolean | string | null;
preferDisc?: boolean;
preferLogo?: boolean;
scalable?: boolean;
shape?: string | null;
lazy?: boolean;
cardLayout?: boolean | string;
cardLayout?: boolean | null;
showParentTitle?: boolean;
showParentTitleOrTitle?: boolean;
showAirTime?: boolean;
@ -37,7 +41,7 @@ export interface CardOptions {
action?: string | null;
defaultShape?: string;
indexBy?: string;
parentId?: string | null;
parentId?: ParentId;
showMenu?: boolean;
cardCssClass?: string | null;
cardClass?: string | null;
@ -61,9 +65,10 @@ export interface CardOptions {
showSeriesTimerChannel?: boolean;
showSongCount?: boolean;
width?: number;
widths?: any;
showChannelLogo?: boolean;
showLogo?: boolean;
serverId?: string;
serverId?: NullableString;
collectionId?: string | null;
playlistId?: string | null;
defaultCardImageIcon?: string;
@ -72,4 +77,46 @@ export interface CardOptions {
showGroupCount?: boolean;
containerClass?: 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[]
}