Files
stash/ui/v2.5/src/components/Scenes/SceneCard.tsx
2021-04-15 11:33:20 +10:00

402 lines
11 KiB
TypeScript

import React, { useEffect, useRef } from "react";
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom";
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import {
Icon,
TagLink,
HoverPopover,
SweatDrops,
TruncatedText,
} from "src/components/Shared";
import { TextUtils } from "src/utils";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
interface IScenePreviewProps {
isPortrait: boolean;
image?: string;
video?: string;
soundActive: boolean;
}
export const ScenePreview: React.FC<IScenePreviewProps> = ({
image,
video,
isPortrait,
soundActive,
}) => {
const videoEl = useRef<HTMLVideoElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0)
// Catch is necessary due to DOMException if user hovers before clicking on page
videoEl.current?.play().catch(() => {});
else videoEl.current?.pause();
});
});
if (videoEl.current) observer.observe(videoEl.current);
});
useEffect(() => {
if (videoEl?.current?.volume)
videoEl.current.volume = soundActive ? 0.05 : 0;
}, [soundActive]);
return (
<div className={cx("scene-card-preview", { portrait: isPortrait })}>
<img className="scene-card-preview-image" src={image} alt="" />
<video
disableRemotePlayback
playsInline
className="scene-card-preview-video"
loop
preload="none"
ref={videoEl}
src={video}
/>
</div>
);
};
interface ISceneCardProps {
scene: GQL.SlimSceneDataFragment;
compact?: boolean;
selecting?: boolean;
selected?: boolean | undefined;
zoomIndex?: number;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
onSceneClicked?: () => void;
}
export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps
) => {
const config = useConfiguration();
// studio image is missing if it uses the default
const missingStudioImage = props.scene.studio?.image_path?.endsWith(
"?default=true"
);
const showStudioAsText =
missingStudioImage ||
(config?.data?.configuration.interface.showStudioAsText ?? false);
function maybeRenderRatingBanner() {
if (!props.scene.rating) {
return;
}
return (
<div
className={`rating-banner ${
props.scene.rating ? `rating-${props.scene.rating}` : ""
}`}
>
RATING: {props.scene.rating}
</div>
);
}
function maybeRenderSceneSpecsOverlay() {
return (
<div className="scene-specs-overlay">
{props.scene.file.width && props.scene.file.height ? (
<span className="overlay-resolution">
{" "}
{TextUtils.resolution(
props.scene.file.width,
props.scene.file.height
)}
</span>
) : (
""
)}
{(props.scene.file.duration ?? 0) >= 1
? TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)
: ""}
</div>
);
}
function maybeRenderSceneStudioOverlay() {
if (!props.scene.studio) return;
return (
<div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`}>
{showStudioAsText ? (
props.scene.studio.name
) : (
<img
className="image-thumbnail"
alt={props.scene.studio.name}
src={props.scene.studio.image_path ?? ""}
/>
)}
</Link>
</div>
);
}
function maybeRenderTagPopoverButton() {
if (props.scene.tags.length <= 0) return;
const popoverContent = props.scene.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} />
));
return (
<HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal">
<Icon icon="tag" />
<span>{props.scene.tags.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderPerformerPopoverButton() {
if (props.scene.performers.length <= 0) return;
return <PerformerPopoverButton performers={props.scene.performers} />;
}
function maybeRenderMoviePopoverButton() {
if (props.scene.movies.length <= 0) return;
const popoverContent = props.scene.movies.map((sceneMovie) => (
<div className="movie-tag-container row" key="movie">
<Link
to={`/movies/${sceneMovie.movie.id}`}
className="movie-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneMovie.movie.name ?? ""}
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<TagLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
className="d-block"
/>
</div>
));
return (
<HoverPopover
placement="bottom"
content={popoverContent}
className="tag-tooltip"
>
<Button className="minimal">
<Icon icon="film" />
<span>{props.scene.movies.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderSceneMarkerPopoverButton() {
if (props.scene.scene_markers.length <= 0) return;
const popoverContent = props.scene.scene_markers.map((marker) => {
const markerPopover = { ...marker, scene: { id: props.scene.id } };
return <TagLink key={marker.id} marker={markerPopover} />;
});
return (
<HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal">
<Icon icon="map-marker-alt" />
<span>{props.scene.scene_markers.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderOCounter() {
if (props.scene.o_counter) {
return (
<div>
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.scene.o_counter}</span>
</Button>
</div>
);
}
}
function maybeRenderGallery() {
if (props.scene.galleries.length <= 0) return;
const popoverContent = props.scene.galleries.map((gallery) => (
<TagLink key={gallery.id} gallery={gallery} />
));
return (
<HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal">
<Icon icon="images" />
<span>{props.scene.galleries.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderOrganized() {
if (props.scene.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
!props.compact &&
(props.scene.tags.length > 0 ||
props.scene.performers.length > 0 ||
props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter ||
props.scene.galleries.length > 0 ||
props.scene.organized)
) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderMoviePopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);
}
}
function handleSceneClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
const { shiftKey } = event;
if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, shiftKey);
event.preventDefault();
} else if (props.onSceneClicked) {
props.onSceneClicked();
event.preventDefault();
}
}
function handleDrag(event: React.DragEvent<HTMLAnchorElement>) {
if (props.selecting) {
event.dataTransfer.setData("text/plain", "");
event.dataTransfer.setDragImage(new Image(), 0, 0);
}
}
function handleDragOver(event: React.DragEvent<HTMLAnchorElement>) {
const ev = event;
const shiftKey = false;
if (props.selecting && props.onSelectedChanged && !props.selected) {
props.onSelectedChanged(true, shiftKey);
}
ev.dataTransfer.dropEffect = "move";
ev.preventDefault();
}
function isPortrait() {
const { file } = props.scene;
const width = file.width ? file.width : 0;
const height = file.height ? file.height : 0;
return height > width;
}
function zoomIndex() {
if (!props.compact && props.zoomIndex !== undefined) {
return `zoom-${props.zoomIndex}`;
}
}
let shiftKey = false;
return (
<Card className={`scene-card ${zoomIndex()}`}>
<Form.Control
type="checkbox"
className="scene-card-check"
checked={props.selected}
onChange={() => props.onSelectedChanged?.(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
<div className="video-section">
<Link
to={`/scenes/${props.scene.id}`}
className="scene-card-link"
onClick={handleSceneClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.selecting}
>
<ScenePreview
image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined}
isPortrait={isPortrait()}
soundActive={
config.data?.configuration?.interface?.soundOnPreview ?? false
}
/>
{maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()}
</Link>
{maybeRenderSceneStudioOverlay()}
</div>
<div className="card-section">
<h5 className="card-section-title">
<Link to={`/scenes/${props.scene.id}`}>
<TruncatedText
text={
props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)
}
lineCount={2}
/>
</Link>
</h5>
<span>{props.scene.date}</span>
<p>
<TruncatedText text={props.scene.details} lineCount={3} />
</p>
</div>
{maybeRenderPopoverButtonGroup()}
</Card>
);
};