mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-18 13:22:07 +00:00
init 2 part media card
Part 1 is the default Part 2 is more options pane
This commit is contained in:
parent
5160ebc189
commit
6cb781e452
3 changed files with 261 additions and 145 deletions
|
|
@ -85,16 +85,6 @@ html[data-no-scroll], html[data-no-scroll] body {
|
|||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
display: inline-block;
|
||||
padding: 0.75em;
|
||||
margin: -0.75em;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(-15px, -10px)
|
||||
}
|
||||
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
:root {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export enum Icons {
|
|||
STRETCH = "stretch",
|
||||
CLOUD_ARROW_UP = "cloud_arrow_up",
|
||||
FILE_ARROW_DOWN = "file_arrow_down",
|
||||
ELLIPSIS = "ellipsis",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
|
|
@ -161,6 +162,7 @@ const iconList: Record<Icons, string> = {
|
|||
stretch: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M344 0L488 0c13.3 0 24 10.7 24 24l0 144c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-39-39-87 87c-9.4 9.4-24.6 9.4-33.9 0l-32-32c-9.4-9.4-9.4-24.6 0-33.9l87-87L327 41c-6.9-6.9-8.9-17.2-5.2-26.2S334.3 0 344 0zM168 512L24 512c-13.3 0-24-10.7-24-24L0 344c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2l39 39 87-87c9.4-9.4 24.6-9.4 33.9 0l32 32c9.4 9.4 9.4 24.6 0 33.9l-87 87 39 39c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8z"/></svg>`,
|
||||
cloud_arrow_up: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z" fill="currentColor"/></svg>`,
|
||||
file_arrow_down: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM216 232V334.1l31-31c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-72 72c-9.4 9.4-24.6 9.4-33.9 0l-72-72c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l31 31V232c0-13.3 10.7-24 24-24s24 10.7 24 24z" fill="currentColor"/></svg>`,
|
||||
ellipsis: `<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from "classnames";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
|
@ -48,7 +48,12 @@ function MediaCardContent({
|
|||
percentage,
|
||||
closable,
|
||||
onClose,
|
||||
}: MediaCardProps) {
|
||||
overlayVisible,
|
||||
setOverlayVisible,
|
||||
}: MediaCardProps & {
|
||||
overlayVisible: boolean;
|
||||
setOverlayVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
||||
|
||||
|
|
@ -68,142 +73,255 @@ function MediaCardContent({
|
|||
dotListContent.push(t("media.unreleased"));
|
||||
}
|
||||
|
||||
const handleMoreInfoClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const searchParam = encodeURIComponent(encodeURI(media.id));
|
||||
const url =
|
||||
media.type === "movie"
|
||||
? `https://www.themoviedb.org/movie/${searchParam}`
|
||||
: `https://www.themoviedb.org/tv/${searchParam}`;
|
||||
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setOverlayVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flare.Base
|
||||
className={`group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10 ${
|
||||
canLink ? "hover:bg-mediaCard-hoverBackground tabbable" : ""
|
||||
}`}
|
||||
tabIndex={canLink ? 0 : -1}
|
||||
onKeyUp={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
>
|
||||
<Flare.Light
|
||||
flareSize={300}
|
||||
cssColorVar="--colors-mediaCard-hoverAccent"
|
||||
backgroundClass="bg-mediaCard-hoverBackground duration-100"
|
||||
className={classNames({
|
||||
"rounded-xl bg-background-main group-hover:opacity-100": canLink,
|
||||
})}
|
||||
/>
|
||||
<Flare.Child
|
||||
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
|
||||
canLink ? "group-hover:scale-95" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
|
||||
{
|
||||
"group-hover:rounded-lg": canLink,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
|
||||
}}
|
||||
<div>
|
||||
{!overlayVisible ? (
|
||||
<Flare.Base
|
||||
className={`group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10 ${
|
||||
canLink ? "hover:bg-mediaCard-hoverBackground tabbable" : ""
|
||||
}`}
|
||||
tabIndex={canLink ? 0 : -1}
|
||||
onKeyUp={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
>
|
||||
{series ? (
|
||||
<div
|
||||
className={[
|
||||
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
<p
|
||||
className={[
|
||||
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
|
||||
closable ? "" : "group-hover:text-white",
|
||||
].join(" ")}
|
||||
>
|
||||
{t("media.episodeDisplay", {
|
||||
season: series.season || 1,
|
||||
episode: series.episode,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{percentage !== undefined ? (
|
||||
<>
|
||||
<div
|
||||
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
|
||||
style={{
|
||||
width: percentageString,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="absolute bookmark-button"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<MediaBookmarkButton media={media} />
|
||||
</div>
|
||||
|
||||
{searchQuery.length > 0 ? (
|
||||
<div className="absolute" onClick={(e) => e.preventDefault()}>
|
||||
<MediaBookmarkButton media={media} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={`absolute inset-0 flex items-center justify-center bg-mediaCard-badge bg-opacity-80 transition-opacity duration-500 ${
|
||||
closable ? "opacity-100" : "pointer-events-none opacity-0"
|
||||
<Flare.Light
|
||||
flareSize={300}
|
||||
cssColorVar="--colors-mediaCard-hoverAccent"
|
||||
backgroundClass="bg-mediaCard-hoverBackground duration-100"
|
||||
className={classNames({
|
||||
"rounded-xl bg-background-main group-hover:opacity-100": canLink,
|
||||
})}
|
||||
/>
|
||||
<Flare.Child
|
||||
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
|
||||
canLink ? "group-hover:scale-95" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<IconPatch
|
||||
clickable
|
||||
className="text-2xl text-mediaCard-badgeText transition-transform hover:scale-110 duration-500"
|
||||
onClick={() => closable && onClose?.()}
|
||||
icon={Icons.X}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
|
||||
<span>{media.title}</span>
|
||||
</h1>
|
||||
<div className="media-info-container justify-content-center flex flex-wrap">
|
||||
<DotList className="text-xs" content={dotListContent} />
|
||||
<button
|
||||
className="info-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
<div
|
||||
className={classNames(
|
||||
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
|
||||
{
|
||||
"group-hover:rounded-lg": canLink,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: media.poster
|
||||
? `url(${media.poster})`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{series ? (
|
||||
<div
|
||||
className={[
|
||||
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
<p
|
||||
className={[
|
||||
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
|
||||
closable ? "" : "group-hover:text-white",
|
||||
].join(" ")}
|
||||
>
|
||||
{t("media.episodeDisplay", {
|
||||
season: series.season || 1,
|
||||
episode: series.episode,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
const searchParam = encodeURIComponent(encodeURI(media.id));
|
||||
const url =
|
||||
media.type === "movie"
|
||||
? `https://www.themoviedb.org/movie/${searchParam}`
|
||||
: `https://www.themoviedb.org/tv/${searchParam}`;
|
||||
{percentage !== undefined ? (
|
||||
<>
|
||||
<div
|
||||
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
|
||||
style={{
|
||||
width: percentageString,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="text-xs font-semibold text-type-secondary"
|
||||
icon={Icons.CIRCLE_QUESTION}
|
||||
<div
|
||||
className="absolute bookmark-button"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<MediaBookmarkButton media={media} />
|
||||
</div>
|
||||
|
||||
{searchQuery.length > 0 ? (
|
||||
<div className="absolute" onClick={(e) => e.preventDefault()}>
|
||||
<MediaBookmarkButton media={media} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={`absolute inset-0 flex items-center justify-center bg-mediaCard-badge bg-opacity-80 transition-opacity duration-500 ${
|
||||
closable ? "opacity-100" : "pointer-events-none opacity-0"
|
||||
}`}
|
||||
>
|
||||
<IconPatch
|
||||
clickable
|
||||
className="text-2xl text-mediaCard-badgeText transition-transform hover:scale-110 duration-500"
|
||||
onClick={() => closable && onClose?.()}
|
||||
icon={Icons.X}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
|
||||
<span>{media.title}</span>
|
||||
</h1>
|
||||
<div className="media-info-container justify-content-center flex flex-wrap">
|
||||
<DotList className="text-xs" content={dotListContent} />
|
||||
</div>
|
||||
|
||||
{/* More Info */}
|
||||
<div className="absolute bottom-1 right-2">
|
||||
<button
|
||||
className="media-more-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOverlayVisible(!overlayVisible);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="text-xs font-semibold text-type-secondary"
|
||||
icon={Icons.ELLIPSIS}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/* End Overlay */}
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
) : (
|
||||
<div onMouseLeave={handleMouseLeave}>
|
||||
<Flare.Base className="group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10">
|
||||
<Flare.Light
|
||||
flareSize={300}
|
||||
cssColorVar="--colors-mediaCard-hoverAccent"
|
||||
backgroundClass="bg-mediaCard-hoverBackground duration-100"
|
||||
className={classNames({
|
||||
"rounded-xl bg-background-main group-hover:opacity-100":
|
||||
canLink,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<Flare.Child
|
||||
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
|
||||
canLink ? "group-hover:scale-95" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
|
||||
{
|
||||
"group-hover:rounded-lg": canLink,
|
||||
},
|
||||
"blur-sm",
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: media.poster
|
||||
? `url(${media.poster})`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{series ? (
|
||||
<div
|
||||
className={[
|
||||
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
<p
|
||||
className={[
|
||||
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
|
||||
closable ? "" : "group-hover:text-white",
|
||||
].join(" ")}
|
||||
>
|
||||
{t("media.episodeDisplay", {
|
||||
season: series.season || 1,
|
||||
episode: series.episode,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{percentage !== undefined ? (
|
||||
<>
|
||||
<div
|
||||
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
|
||||
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
|
||||
style={{
|
||||
width: percentageString,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
|
||||
<span>{media.title}</span>
|
||||
</h1>
|
||||
<div className="media-info-container justify-content-center flex flex-wrap">
|
||||
<DotList className="text-xs" content={dotListContent} />
|
||||
</div>
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
</div>
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaCard(props: MediaCardProps) {
|
||||
const content = <MediaCardContent {...props} />;
|
||||
const [overlayVisible, setOverlayVisible] = useState(false);
|
||||
|
||||
const content = (
|
||||
<MediaCardContent
|
||||
{...props}
|
||||
overlayVisible={overlayVisible}
|
||||
setOverlayVisible={setOverlayVisible}
|
||||
/>
|
||||
);
|
||||
|
||||
const isReleased = useCallback(
|
||||
() => checkReleased(props.media),
|
||||
|
|
@ -227,15 +345,21 @@ export function MediaCard(props: MediaCardProps) {
|
|||
|
||||
if (!canLink) return <span>{content}</span>;
|
||||
return (
|
||||
<Link
|
||||
to={link}
|
||||
tabIndex={-1}
|
||||
className={classNames(
|
||||
"tabbable",
|
||||
props.closable ? "hover:cursor-default" : "",
|
||||
<div className="relative">
|
||||
{!overlayVisible ? (
|
||||
<Link
|
||||
to={link}
|
||||
tabIndex={-1}
|
||||
className={classNames(
|
||||
"tabbable",
|
||||
props.closable ? "hover:cursor-default" : "",
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue