mirror of
https://github.com/stashapp/stash.git
synced 2026-05-08 20:58:48 +02:00
Gallery scrubber wall view (#5191)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
8c2a25b833
commit
5721ea2b70
3 changed files with 83 additions and 31 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
|
@ -7,32 +7,50 @@ import TextUtils from "src/utils/text";
|
|||
import { useGalleryLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import { GalleryPreviewScrubber } from "./GalleryPreviewScrubber";
|
||||
import cx from "classnames";
|
||||
|
||||
const CLASSNAME = "GalleryWallCard";
|
||||
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
||||
const CLASSNAME_IMG = `${CLASSNAME}-img`;
|
||||
const CLASSNAME_TITLE = `${CLASSNAME}-title`;
|
||||
const CLASSNAME_IMG_CONTAIN = `${CLASSNAME}-img-contain`;
|
||||
|
||||
interface IProps {
|
||||
gallery: GQL.SlimGalleryDataFragment;
|
||||
}
|
||||
|
||||
type Orientation = "landscape" | "portrait";
|
||||
|
||||
function getOrientation(width: number, height: number): Orientation {
|
||||
return width > height ? "landscape" : "portrait";
|
||||
}
|
||||
|
||||
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||
const intl = useIntl();
|
||||
const [orientation, setOrientation] = React.useState<
|
||||
"landscape" | "portrait"
|
||||
>("landscape");
|
||||
const [coverOrientation, setCoverOrientation] =
|
||||
React.useState<Orientation>("landscape");
|
||||
const [imageOrientation, setImageOrientation] =
|
||||
React.useState<Orientation>("landscape");
|
||||
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
|
||||
|
||||
const cover = gallery?.paths.cover;
|
||||
|
||||
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {
|
||||
function onCoverLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {
|
||||
const target = e.target as HTMLImageElement;
|
||||
setOrientation(
|
||||
target.naturalWidth > target.naturalHeight ? "landscape" : "portrait"
|
||||
setCoverOrientation(
|
||||
getOrientation(target.naturalWidth, target.naturalHeight)
|
||||
);
|
||||
}
|
||||
|
||||
function onNonCoverLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {
|
||||
const target = e.target as HTMLImageElement;
|
||||
setImageOrientation(
|
||||
getOrientation(target.naturalWidth, target.naturalHeight)
|
||||
);
|
||||
}
|
||||
|
||||
const [imgSrc, setImgSrc] = useState<string | undefined>(cover ?? undefined);
|
||||
const title = galleryTitle(gallery);
|
||||
const performerNames = gallery.performers.map((p) => p.name);
|
||||
const performers =
|
||||
|
|
@ -48,10 +66,13 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
|||
showLightbox(0);
|
||||
}
|
||||
|
||||
const imgClassname =
|
||||
imageOrientation !== coverOrientation ? CLASSNAME_IMG_CONTAIN : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${CLASSNAME} ${CLASSNAME}-${orientation}`}
|
||||
className={`${CLASSNAME} ${CLASSNAME}-${coverOrientation}`}
|
||||
onClick={showLightboxStart}
|
||||
onKeyPress={showLightboxStart}
|
||||
role="button"
|
||||
|
|
@ -60,29 +81,41 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
|||
<RatingSystem value={gallery.rating100} disabled withoutContext />
|
||||
<img
|
||||
loading="lazy"
|
||||
src={cover}
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
className={CLASSNAME_IMG}
|
||||
onLoad={onImageLoad}
|
||||
className={cx(CLASSNAME_IMG, imgClassname)}
|
||||
// set orientation based on cover only
|
||||
onLoad={imgSrc === cover ? onCoverLoad : onNonCoverLoad}
|
||||
/>
|
||||
<footer className={CLASSNAME_FOOTER}>
|
||||
<Link
|
||||
to={`/galleries/${gallery.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<TruncatedText
|
||||
text={title}
|
||||
lineCount={1}
|
||||
className={CLASSNAME_TITLE}
|
||||
/>
|
||||
)}
|
||||
<TruncatedText text={performers.join(", ")} />
|
||||
<div>
|
||||
{gallery.date && TextUtils.formatDate(intl, gallery.date)}
|
||||
</div>
|
||||
</Link>
|
||||
</footer>
|
||||
<div className="lineargradient">
|
||||
<footer className={CLASSNAME_FOOTER}>
|
||||
<Link
|
||||
to={`/galleries/${gallery.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<TruncatedText
|
||||
text={title}
|
||||
lineCount={1}
|
||||
className={CLASSNAME_TITLE}
|
||||
/>
|
||||
)}
|
||||
<TruncatedText text={performers.join(", ")} />
|
||||
<div>
|
||||
{gallery.date && TextUtils.formatDate(intl, gallery.date)}
|
||||
</div>
|
||||
</Link>
|
||||
</footer>
|
||||
<GalleryPreviewScrubber
|
||||
previewPath={gallery.paths.preview}
|
||||
defaultPath={cover ?? ""}
|
||||
imageCount={gallery.image_count}
|
||||
onClick={(i) => {
|
||||
showLightbox(i);
|
||||
}}
|
||||
onPathChanged={setImgSrc}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -237,6 +237,18 @@ $galleryTabWidth: 450px;
|
|||
width: 96vw;
|
||||
}
|
||||
|
||||
.lineargradient {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
||||
bottom: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-scrubber {
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@mixin galleryWidth($width) {
|
||||
height: math.div($width, 3) * 2;
|
||||
|
||||
|
|
@ -264,6 +276,11 @@ $galleryTabWidth: 450px;
|
|||
object-fit: cover;
|
||||
object-position: center 20%;
|
||||
width: 100%;
|
||||
|
||||
&.GalleryWallCard-img-contain {
|
||||
object-fit: contain;
|
||||
object-position: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
|
|
@ -271,13 +288,13 @@ $galleryTabWidth: 450px;
|
|||
}
|
||||
|
||||
&-footer {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
||||
bottom: 0;
|
||||
bottom: 20px;
|
||||
padding: 1rem;
|
||||
position: absolute;
|
||||
text-shadow: 1px 1px 3px black;
|
||||
transition: 0s opacity;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
opacity: 0;
|
||||
|
|
@ -310,6 +327,7 @@ $galleryTabWidth: 450px;
|
|||
right: 1rem;
|
||||
text-shadow: 1px 1px 3px black;
|
||||
top: 1rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export const HoverScrubber: React.FC<IHoverScrubber> = ({
|
|||
return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue