140
resources/client/titles/title-poster/title-backdrop.tsx
Executable file
140
resources/client/titles/title-poster/title-backdrop.tsx
Executable file
@@ -0,0 +1,140 @@
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {message} from '@common/i18n/message';
|
||||
import clsx from 'clsx';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {
|
||||
ImageSize,
|
||||
useImageSrc,
|
||||
useImageSrcSet,
|
||||
} from '@app/images/use-image-src';
|
||||
import {Episode} from '@app/titles/models/episode';
|
||||
import {TitleLink} from '@app/titles/title-link';
|
||||
import {EpisodeLink} from '@app/episodes/episode-link';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {getWatchLink} from '@app/videos/watch-page/get-watch-link';
|
||||
import {MediaPlayIcon} from '@common/icons/media/media-play';
|
||||
import {MovieIcon} from '@common/icons/material/Movie';
|
||||
|
||||
// can provide either url for backdrop directly or
|
||||
// title/episode object if main backdrop for it should be used
|
||||
interface Props {
|
||||
src?: string;
|
||||
title?: Title;
|
||||
episode?: Episode;
|
||||
className?: string;
|
||||
size?: string;
|
||||
lazy?: boolean;
|
||||
srcSize?: ImageSize;
|
||||
wrapWithLink?: boolean;
|
||||
showPlayButton?: boolean;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
export function TitleBackdrop({
|
||||
src: initialSrc,
|
||||
title,
|
||||
episode,
|
||||
className,
|
||||
size,
|
||||
srcSize,
|
||||
lazy = true,
|
||||
wrapWithLink = false,
|
||||
showPlayButton,
|
||||
wrapperClassName,
|
||||
}: Props) {
|
||||
const {trans} = useTrans();
|
||||
const primaryVideo = episode?.primary_video || title?.primary_video;
|
||||
if (!primaryVideo) {
|
||||
showPlayButton = false;
|
||||
}
|
||||
|
||||
if (!initialSrc && episode) {
|
||||
initialSrc = episode?.poster;
|
||||
}
|
||||
if (!initialSrc && title) {
|
||||
initialSrc = title.backdrop;
|
||||
}
|
||||
|
||||
const src = useImageSrc(initialSrc, {size: srcSize});
|
||||
const item = episode || title;
|
||||
const srcset = useImageSrcSet(initialSrc);
|
||||
|
||||
const imageClassName = clsx(
|
||||
className,
|
||||
size,
|
||||
'aspect-video bg-fg-base/4 object-cover',
|
||||
!src ? 'flex items-center justify-center' : 'block',
|
||||
);
|
||||
|
||||
let img = src ? (
|
||||
<img
|
||||
className={imageClassName}
|
||||
draggable={false}
|
||||
decoding="async"
|
||||
sizes={!srcSize ? `100vw` : undefined}
|
||||
loading={lazy ? 'lazy' : 'eager'}
|
||||
src={src}
|
||||
srcSet={!srcSize ? srcset : undefined}
|
||||
alt={
|
||||
item
|
||||
? trans(
|
||||
message('Backdrop for :name', {
|
||||
values: {name: item.name},
|
||||
}),
|
||||
)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className={imageClassName}>
|
||||
<MovieIcon className="max-w-[60%] text-divider" size="text-6xl" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const playButton = showPlayButton ? (
|
||||
<div className="absolute bottom-14 left-14">
|
||||
<IconButton
|
||||
color="white"
|
||||
variant="flat"
|
||||
className="shadow-md"
|
||||
radius="rounded-full"
|
||||
elementType={Link}
|
||||
to={getWatchLink(primaryVideo!)}
|
||||
aria-label="Play"
|
||||
>
|
||||
<MediaPlayIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (wrapWithLink) {
|
||||
if (episode) {
|
||||
img = (
|
||||
<EpisodeLink
|
||||
episode={episode}
|
||||
title={title!}
|
||||
seasonNumber={episode.season_number}
|
||||
displayContents
|
||||
>
|
||||
{img}
|
||||
</EpisodeLink>
|
||||
);
|
||||
} else if (title) {
|
||||
img = (
|
||||
<TitleLink title={title} displayContents>
|
||||
{img}
|
||||
</TitleLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('group relative flex-shrink-0', wrapperClassName)}>
|
||||
{img}
|
||||
{playButton}
|
||||
{wrapWithLink && (
|
||||
<div className="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity group-hover:opacity-10" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
resources/client/titles/title-poster/title-poster.tsx
Executable file
97
resources/client/titles/title-poster/title-poster.tsx
Executable file
@@ -0,0 +1,97 @@
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {message} from '@common/i18n/message';
|
||||
import clsx from 'clsx';
|
||||
import {Title} from '@app/titles/models/title';
|
||||
import {TitleLink} from '@app/titles/title-link';
|
||||
import {ImageSize, useImageSrc} from '@app/images/use-image-src';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {getWatchLink} from '@app/videos/watch-page/get-watch-link';
|
||||
import {MediaPlayIcon} from '@common/icons/media/media-play';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {Fragment} from 'react';
|
||||
import {MovieIcon} from '@common/icons/material/Movie';
|
||||
|
||||
interface Props {
|
||||
title: Title;
|
||||
className?: string;
|
||||
size?: string;
|
||||
lazy?: boolean;
|
||||
srcSize?: ImageSize;
|
||||
aspect?: string;
|
||||
showPlayButton?: boolean;
|
||||
link?: string;
|
||||
}
|
||||
export function TitlePoster({
|
||||
title,
|
||||
className,
|
||||
size = 'w-full',
|
||||
srcSize,
|
||||
lazy = true,
|
||||
aspect = 'aspect-poster',
|
||||
showPlayButton,
|
||||
link,
|
||||
}: Props) {
|
||||
const {trans} = useTrans();
|
||||
const src = useImageSrc(title?.poster, {size: srcSize});
|
||||
if (!title.primary_video) {
|
||||
showPlayButton = false;
|
||||
}
|
||||
|
||||
const imageClassName = clsx(
|
||||
'h-full w-full rounded bg-fg-base/4 object-cover',
|
||||
!src ? 'flex items-center justify-center' : 'block',
|
||||
);
|
||||
|
||||
const image = src ? (
|
||||
<img
|
||||
className={imageClassName}
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
loading={lazy ? 'lazy' : 'eager'}
|
||||
src={src}
|
||||
alt={trans(message('Poster for :name', {values: {name: title.name}}))}
|
||||
/>
|
||||
) : (
|
||||
<span className={clsx(imageClassName, 'overflow-hidden')}>
|
||||
<MovieIcon className="max-w-[60%] text-divider" size="text-6xl" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const linkChildren = (
|
||||
<Fragment>
|
||||
{image}
|
||||
<span className="pointer-events-none absolute inset-0 block bg-black opacity-0 transition-opacity group-hover:opacity-10" />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(size, aspect, className, 'group relative flex-shrink-0')}
|
||||
>
|
||||
{link ? (
|
||||
<Link to={link} className="contents">
|
||||
{linkChildren}
|
||||
</Link>
|
||||
) : (
|
||||
<TitleLink title={title} displayContents>
|
||||
{linkChildren}
|
||||
</TitleLink>
|
||||
)}
|
||||
{showPlayButton ? (
|
||||
<div className="absolute bottom-14 left-14">
|
||||
<IconButton
|
||||
color="white"
|
||||
variant="flat"
|
||||
className="shadow-md"
|
||||
radius="rounded-full"
|
||||
elementType={Link}
|
||||
to={getWatchLink(title.primary_video)}
|
||||
aria-label={`Play ${title.name}`}
|
||||
>
|
||||
<MediaPlayIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user