Scene card preview refactor (#787)

* Refactor scene card preview
* Add delay to video preview
This commit is contained in:
InfiniteTF
2020-09-11 02:52:36 +02:00
committed by GitHub
parent 629126df98
commit 9095ba21dc
5 changed files with 99 additions and 133 deletions

View File

@@ -1,13 +1,62 @@
import React, { useState } from "react"; import React, { useEffect, useRef } from "react";
import { Button, ButtonGroup, Card, Form } from "react-bootstrap"; import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService"; import { useConfiguration } from "src/core/StashService";
import { useVideoHover } from "src/hooks";
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared"; import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
interface IScenePreviewProps {
isPortrait: boolean;
image?: string;
video?: string;
soundActive: boolean;
}
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();
});
},
{ root: document.documentElement }
);
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
className="scene-card-preview-video"
loop
preload="none"
ref={videoEl}
src={video}
/>
</div>
);
};
interface ISceneCardProps { interface ISceneCardProps {
scene: GQL.SlimSceneDataFragment; scene: GQL.SlimSceneDataFragment;
selecting?: boolean; selecting?: boolean;
@@ -19,11 +68,6 @@ interface ISceneCardProps {
export const SceneCard: React.FC<ISceneCardProps> = ( export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps props: ISceneCardProps
) => { ) => {
const [previewPath, setPreviewPath] = useState<string>();
const hoverHandler = useVideoHover({
resetOnMouseLeave: false,
});
const config = useConfiguration(); const config = useConfiguration();
const showStudioAsText = const showStudioAsText =
config?.data?.configuration.interface.showStudioAsText ?? false; config?.data?.configuration.interface.showStudioAsText ?? false;
@@ -220,18 +264,6 @@ export const SceneCard: React.FC<ISceneCardProps> = (
} }
} }
function onMouseEnter() {
if (!previewPath || previewPath === "") {
setPreviewPath(props.scene.paths.preview || "");
}
hoverHandler.onMouseEnter();
}
function onMouseLeave() {
hoverHandler.onMouseLeave();
setPreviewPath("");
}
function handleSceneClick( function handleSceneClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent> event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) { ) {
@@ -272,11 +304,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
let shiftKey = false; let shiftKey = false;
return ( return (
<Card <Card className={`scene-card zoom-${props.zoomIndex}`}>
className={`scene-card zoom-${props.zoomIndex}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<Form.Control <Form.Control
type="checkbox" type="checkbox"
className="scene-card-check" className="scene-card-check"
@@ -298,16 +326,16 @@ export const SceneCard: React.FC<ISceneCardProps> = (
onDragOver={handleDragOver} onDragOver={handleDragOver}
draggable={props.selecting} 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()} {maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()} {maybeRenderSceneSpecsOverlay()}
<video
loop
className={cx("scene-card-video", { portrait: isPortrait() })}
poster={props.scene.paths.screenshot || ""}
ref={hoverHandler.videoEl}
>
{previewPath ? <source src={previewPath} /> : ""}
</video>
</Link> </Link>
{maybeRenderSceneStudioOverlay()} {maybeRenderSceneStudioOverlay()}
</div> </div>

View File

@@ -169,7 +169,7 @@ textarea.scene-description {
padding: 0; padding: 0;
} }
.scene-card-check { &-check {
left: 0.5rem; left: 0.5rem;
margin-top: -12px; margin-top: -12px;
opacity: 0; opacity: 0;
@@ -190,6 +190,34 @@ textarea.scene-description {
transition: opacity 0.5s; transition: opacity 0.5s;
} }
&-preview {
display: flex;
justify-content: center;
margin-bottom: 5px;
position: relative;
&-image,
&-video {
height: 100%;
object-fit: cover;
width: 100%;
}
&-video {
position: absolute;
top: -9999px;
transition: top 0s;
transition-delay: 0s;
}
&.portrait {
.scene-card-preview-image,
.scene-card-preview-video {
object-fit: contain;
}
}
}
&:hover { &:hover {
.scene-specs-overlay, .scene-specs-overlay,
.rating-banner, .rating-banner,
@@ -207,6 +235,11 @@ textarea.scene-description {
opacity: 0.75; opacity: 0.75;
transition: opacity 0.5s; transition: opacity 0.5s;
} }
.scene-card-preview-video {
top: 0;
transition-delay: 0.2s;
}
} }
} }

View File

@@ -1,86 +0,0 @@
import { useEffect, useRef } from "react";
import { useConfiguration } from "../core/StashService";
export interface IVideoHoverHookData {
videoEl: React.RefObject<HTMLVideoElement>;
isPlaying: React.MutableRefObject<boolean>;
isHovering: React.MutableRefObject<boolean>;
options: IVideoHoverHookOptions;
}
export interface IVideoHoverHookOptions {
resetOnMouseLeave: boolean;
}
export const useVideoHover = (options: IVideoHoverHookOptions) => {
const videoEl = useRef<HTMLVideoElement>(null);
const isPlaying = useRef<boolean>(false);
const isHovering = useRef<boolean>(false);
const config = useConfiguration();
const onMouseEnter = () => {
isHovering.current = true;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (videoTag.paused && !isPlaying.current) {
videoTag.play().catch(() => {});
}
};
const onMouseLeave = () => {
isHovering.current = false;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (!videoTag.paused && isPlaying) {
videoTag.pause();
if (options.resetOnMouseLeave) {
videoTag.removeAttribute("src");
videoTag.load();
isPlaying.current = false;
}
}
};
const soundEnabled =
config?.data?.configuration?.interface?.soundOnPreview ?? true;
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
videoTag.onplaying = () => {
if (isHovering.current === true) {
isPlaying.current = true;
} else {
videoTag.pause();
}
};
videoTag.onpause = () => {
isPlaying.current = false;
};
}, [videoEl]);
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
videoTag.volume = soundEnabled ? 0.05 : 0;
}, [soundEnabled]);
return {
videoEl,
isPlaying,
isHovering,
options,
onMouseEnter,
onMouseLeave,
};
};

View File

@@ -1,6 +1,5 @@
export { default as useToast } from "./Toast"; export { default as useToast } from "./Toast";
export { useInterfaceLocalForage, useChangelogStorage } from "./LocalForage"; export { useInterfaceLocalForage, useChangelogStorage } from "./LocalForage";
export { useVideoHover } from "./VideoHover";
export { export {
useScenesList, useScenesList,
useSceneMarkersList, useSceneMarkersList,

View File

@@ -99,7 +99,7 @@ textarea.text-input {
.zoom-0 { .zoom-0 {
width: 240px; width: 240px;
.scene-card-video { .scene-card-preview {
height: 135px; height: 135px;
} }
@@ -116,7 +116,7 @@ textarea.text-input {
.zoom-1 { .zoom-1 {
width: 320px; width: 320px;
.scene-card-video { .scene-card-preview {
height: 180px; height: 180px;
} }
@@ -133,7 +133,7 @@ textarea.text-input {
.zoom-2 { .zoom-2 {
width: 480px; width: 480px;
.scene-card-video { .scene-card-preview {
height: 270px; height: 270px;
} }
@@ -150,7 +150,7 @@ textarea.text-input {
.zoom-3 { .zoom-3 {
width: 640px; width: 640px;
.scene-card-video { .scene-card-preview {
height: 360px; height: 360px;
} }
@@ -165,15 +165,7 @@ textarea.text-input {
} }
} }
.scene-card-video { .scene-card-preview,
object-fit: cover;
&.portrait {
object-fit: contain;
}
}
.scene-card-video,
.gallery-card-image, .gallery-card-image,
.tag-card-image { .tag-card-image {
height: auto; height: auto;