mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Lazy load images (#4228)
* Add lazy loading for many images * Load sprites on first hover of scrubber
This commit is contained in:
@@ -99,6 +99,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
|
loading="lazy"
|
||||||
alt={props.gallery.studio.name}
|
alt={props.gallery.studio.name}
|
||||||
src={props.gallery.studio.image_path ?? ""}
|
src={props.gallery.studio.image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
@@ -153,6 +154,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||||||
<>
|
<>
|
||||||
{props.gallery.cover ? (
|
{props.gallery.cover ? (
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="gallery-card-image"
|
className="gallery-card-image"
|
||||||
alt={props.gallery.title ?? ""}
|
alt={props.gallery.title ?? ""}
|
||||||
src={`${props.gallery.cover.paths.thumbnail}`}
|
src={`${props.gallery.cover.paths.thumbnail}`}
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||||||
<Link to={`/galleries/${gallery.id}`}>
|
<Link to={`/galleries/${gallery.id}`}>
|
||||||
{gallery.cover ? (
|
{gallery.cover ? (
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
alt={gallery.title ?? ""}
|
alt={gallery.title ?? ""}
|
||||||
className="w-100 w-sm-auto"
|
className="w-100 w-sm-auto"
|
||||||
src={`${gallery.cover.paths.thumbnail}`}
|
src={`${gallery.cover.paths.thumbnail}`}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<RatingSystem value={gallery.rating100 ?? undefined} disabled />
|
<RatingSystem value={gallery.rating100 ?? undefined} disabled />
|
||||||
<img src={cover} alt="" className={CLASSNAME_IMG} />
|
<img loading="lazy" src={cover} alt="" className={CLASSNAME_IMG} />
|
||||||
<footer className={CLASSNAME_FOOTER}>
|
<footer className={CLASSNAME_FOOTER}>
|
||||||
<Link
|
<Link
|
||||||
to={`/galleries/${gallery.id}`}
|
to={`/galleries/${gallery.id}`}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
image={
|
image={
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="movie-card-image"
|
className="movie-card-image"
|
||||||
alt={props.movie.name ?? ""}
|
alt={props.movie.name ?? ""}
|
||||||
src={props.movie.front_image_path ?? ""}
|
src={props.movie.front_image_path ?? ""}
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
image={
|
image={
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="performer-card-image"
|
className="performer-card-image"
|
||||||
alt={performer.name ?? ""}
|
alt={performer.name ?? ""}
|
||||||
src={performer.image_path ?? ""}
|
src={performer.image_path ?? ""}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
|||||||
<td>
|
<td>
|
||||||
<Link to={`/performers/${performer.id}`}>
|
<Link to={`/performers/${performer.id}`}>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
alt={performer.name ?? ""}
|
alt={performer.name ?? ""}
|
||||||
src={performer.image_path ?? ""}
|
src={performer.image_path ?? ""}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { useRef, useMemo, useState, useLayoutEffect } from "react";
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useLayoutEffect,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
import { useSpriteInfo } from "src/hooks/sprite";
|
import { useSpriteInfo } from "src/hooks/sprite";
|
||||||
import { useThrottle } from "src/hooks/throttle";
|
import { useThrottle } from "src/hooks/throttle";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
interface IHoverScrubber {
|
interface IHoverScrubber {
|
||||||
totalSprites: number;
|
totalSprites: number;
|
||||||
@@ -62,7 +69,11 @@ const HoverScrubber: React.FC<IHoverScrubber> = ({
|
|||||||
}, [activeIndex, totalSprites]);
|
}, [activeIndex, totalSprites]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hover-scrubber">
|
<div
|
||||||
|
className={cx("hover-scrubber", {
|
||||||
|
"hover-scrubber-inactive": !totalSprites,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="hover-scrubber-area"
|
className="hover-scrubber-area"
|
||||||
onMouseMove={onMouseMove}
|
onMouseMove={onMouseMove}
|
||||||
@@ -109,7 +120,9 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
|||||||
|
|
||||||
const debounceSetActiveIndex = useThrottle(setActiveIndex, 50);
|
const debounceSetActiveIndex = useThrottle(setActiveIndex, 50);
|
||||||
|
|
||||||
const spriteInfo = useSpriteInfo(vttPath);
|
// hold off on loading vtt until first mouse over
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
const spriteInfo = useSpriteInfo(hasLoaded ? vttPath : undefined);
|
||||||
|
|
||||||
const sprite = useMemo(() => {
|
const sprite = useMemo(() => {
|
||||||
if (!spriteInfo || activeIndex === undefined) {
|
if (!spriteInfo || activeIndex === undefined) {
|
||||||
@@ -118,6 +131,13 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
|||||||
return spriteInfo[activeIndex];
|
return spriteInfo[activeIndex];
|
||||||
}, [activeIndex, spriteInfo]);
|
}, [activeIndex, spriteInfo]);
|
||||||
|
|
||||||
|
// mark as loaded on the first hover
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndex !== undefined) {
|
||||||
|
setHasLoaded(true);
|
||||||
|
}
|
||||||
|
}, [activeIndex]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const imageParent = imageParentRef.current;
|
const imageParent = imageParentRef.current;
|
||||||
|
|
||||||
@@ -153,7 +173,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
|||||||
onClick(sprite.start);
|
onClick(sprite.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!spriteInfo) return null;
|
if (!spriteInfo && hasLoaded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="preview-scrubber">
|
<div className="preview-scrubber">
|
||||||
@@ -166,7 +186,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<HoverScrubber
|
<HoverScrubber
|
||||||
totalSprites={spriteInfo.length}
|
totalSprites={spriteInfo?.length ?? 0}
|
||||||
activeIndex={activeIndex}
|
activeIndex={activeIndex}
|
||||||
setActiveIndex={(i) => debounceSetActiveIndex(i)}
|
setActiveIndex={(i) => debounceSetActiveIndex(i)}
|
||||||
onClick={onScrubberClick}
|
onClick={onScrubberClick}
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx("scene-card-preview", { portrait: isPortrait })}>
|
<div className={cx("scene-card-preview", { portrait: isPortrait })}>
|
||||||
<img className="scene-card-preview-image" src={image} alt="" />
|
<img
|
||||||
|
className="scene-card-preview-image"
|
||||||
|
loading="lazy"
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<video
|
<video
|
||||||
disableRemotePlayback
|
disableRemotePlayback
|
||||||
playsInline
|
playsInline
|
||||||
@@ -166,7 +171,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img className="image-thumbnail" alt={studioName} src={studioImage} />
|
<img
|
||||||
|
className="image-thumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
alt={studioName}
|
||||||
|
src={studioImage}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,11 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
|
|||||||
>
|
>
|
||||||
<div className="ml-1 d-flex align-items-center">
|
<div className="ml-1 d-flex align-items-center">
|
||||||
<div className="thumbnail-container">
|
<div className="thumbnail-container">
|
||||||
<img alt={scene.title ?? ""} src={scene.paths.screenshot ?? ""} />
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
alt={scene.title ?? ""}
|
||||||
|
src={scene.paths.screenshot ?? ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="align-middle text-break">
|
<span className="align-middle text-break">
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
<td>
|
<td>
|
||||||
<Link to={sceneLink}>
|
<Link to={sceneLink}>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
alt={title}
|
alt={title}
|
||||||
src={scene.paths.screenshot ?? ""}
|
src={scene.paths.screenshot ?? ""}
|
||||||
|
|||||||
@@ -686,6 +686,16 @@ input[type="range"].blue-slider {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hover-scrubber-inactive {
|
||||||
|
.hover-scrubber-area {
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scrubber-indicator {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hover-scrubber-indicator {
|
.hover-scrubber-indicator {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
bottom: -100%;
|
bottom: -100%;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
|
|||||||
if (props.interactiveHeatmap) {
|
if (props.interactiveHeatmap) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
src={props.interactiveHeatmap}
|
src={props.interactiveHeatmap}
|
||||||
alt="interactive heatmap"
|
alt="interactive heatmap"
|
||||||
className="interactive-heatmap"
|
className="interactive-heatmap"
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export const StudioCard: React.FC<IProps> = ({
|
|||||||
linkClassName="studio-card-header"
|
linkClassName="studio-card-header"
|
||||||
image={
|
image={
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="studio-card-image"
|
className="studio-card-image"
|
||||||
alt={studio.name}
|
alt={studio.name}
|
||||||
src={studio.image_path ?? ""}
|
src={studio.image_path ?? ""}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const TaggerSceneDetails: React.FC<ITaggerSceneDetails> = ({ scene }) => {
|
|||||||
className="performer-tag col m-auto zoom-2"
|
className="performer-tag col m-auto zoom-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
alt={performer.name ?? ""}
|
alt={performer.name ?? ""}
|
||||||
src={performer.image_path ?? ""}
|
src={performer.image_path ?? ""}
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
key={p.remote_site_id}
|
key={p.remote_site_id}
|
||||||
onClick={() => setModalStudio(p)}
|
onClick={() => setModalStudio(p)}
|
||||||
>
|
>
|
||||||
<img src={(p.image ?? [])[0]} alt="" className="StudioTagger-thumb" />
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src={(p.image ?? [])[0]}
|
||||||
|
alt=""
|
||||||
|
className="StudioTagger-thumb"
|
||||||
|
/>
|
||||||
<span>{p.name}</span>
|
<span>{p.name}</span>
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
|
|||||||
<div></div>
|
<div></div>
|
||||||
<div>
|
<div>
|
||||||
<Card className="studio-card">
|
<Card className="studio-card">
|
||||||
<img src={studio.image_path ?? ""} alt="" />
|
<img loading="lazy" src={studio.image_path ?? ""} alt="" />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${CLASSNAME}-details-text`}>
|
<div className={`${CLASSNAME}-details-text`}>
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
linkClassName="tag-card-header"
|
linkClassName="tag-card-header"
|
||||||
image={
|
image={
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
className="tag-card-image"
|
className="tag-card-image"
|
||||||
alt={tag.name}
|
alt={tag.name}
|
||||||
src={tag.image_path ?? ""}
|
src={tag.image_path ?? ""}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const Preview: React.FC<{
|
|||||||
|
|
||||||
const image = (
|
const image = (
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
alt=""
|
alt=""
|
||||||
className="wall-item-media"
|
className="wall-item-media"
|
||||||
src={
|
src={
|
||||||
|
|||||||
Reference in New Issue
Block a user