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
|
||||
className="image-thumbnail"
|
||||
loading="lazy"
|
||||
alt={props.gallery.studio.name}
|
||||
src={props.gallery.studio.image_path ?? ""}
|
||||
/>
|
||||
@@ -153,6 +154,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
||||
<>
|
||||
{props.gallery.cover ? (
|
||||
<img
|
||||
loading="lazy"
|
||||
className="gallery-card-image"
|
||||
alt={props.gallery.title ?? ""}
|
||||
src={`${props.gallery.cover.paths.thumbnail}`}
|
||||
|
||||
@@ -168,6 +168,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||
<Link to={`/galleries/${gallery.id}`}>
|
||||
{gallery.cover ? (
|
||||
<img
|
||||
loading="lazy"
|
||||
alt={gallery.title ?? ""}
|
||||
className="w-100 w-sm-auto"
|
||||
src={`${gallery.cover.paths.thumbnail}`}
|
||||
|
||||
@@ -51,7 +51,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||
tabIndex={0}
|
||||
>
|
||||
<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}>
|
||||
<Link
|
||||
to={`/galleries/${gallery.id}`}
|
||||
|
||||
@@ -76,6 +76,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
||||
image={
|
||||
<>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="movie-card-image"
|
||||
alt={props.movie.name ?? ""}
|
||||
src={props.movie.front_image_path ?? ""}
|
||||
|
||||
@@ -266,6 +266,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
image={
|
||||
<>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="performer-card-image"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
||||
<td>
|
||||
<Link to={`/performers/${performer.id}`}>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
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 { useThrottle } from "src/hooks/throttle";
|
||||
import TextUtils from "src/utils/text";
|
||||
import cx from "classnames";
|
||||
|
||||
interface IHoverScrubber {
|
||||
totalSprites: number;
|
||||
@@ -62,7 +69,11 @@ const HoverScrubber: React.FC<IHoverScrubber> = ({
|
||||
}, [activeIndex, totalSprites]);
|
||||
|
||||
return (
|
||||
<div className="hover-scrubber">
|
||||
<div
|
||||
className={cx("hover-scrubber", {
|
||||
"hover-scrubber-inactive": !totalSprites,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="hover-scrubber-area"
|
||||
onMouseMove={onMouseMove}
|
||||
@@ -109,7 +120,9 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
||||
|
||||
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(() => {
|
||||
if (!spriteInfo || activeIndex === undefined) {
|
||||
@@ -118,6 +131,13 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
||||
return spriteInfo[activeIndex];
|
||||
}, [activeIndex, spriteInfo]);
|
||||
|
||||
// mark as loaded on the first hover
|
||||
useEffect(() => {
|
||||
if (activeIndex !== undefined) {
|
||||
setHasLoaded(true);
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const imageParent = imageParentRef.current;
|
||||
|
||||
@@ -153,7 +173,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
||||
onClick(sprite.start);
|
||||
}
|
||||
|
||||
if (!spriteInfo) return null;
|
||||
if (!spriteInfo && hasLoaded) return null;
|
||||
|
||||
return (
|
||||
<div className="preview-scrubber">
|
||||
@@ -166,7 +186,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<HoverScrubber
|
||||
totalSprites={spriteInfo.length}
|
||||
totalSprites={spriteInfo?.length ?? 0}
|
||||
activeIndex={activeIndex}
|
||||
setActiveIndex={(i) => debounceSetActiveIndex(i)}
|
||||
onClick={onScrubberClick}
|
||||
|
||||
@@ -71,7 +71,12 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
|
||||
|
||||
return (
|
||||
<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
|
||||
disableRemotePlayback
|
||||
playsInline
|
||||
@@ -166,7 +171,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
}
|
||||
|
||||
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="thumbnail-container">
|
||||
<img alt={scene.title ?? ""} src={scene.paths.screenshot ?? ""} />
|
||||
<img
|
||||
loading="lazy"
|
||||
alt={scene.title ?? ""}
|
||||
src={scene.paths.screenshot ?? ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="align-middle text-break">
|
||||
|
||||
@@ -87,6 +87,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||
<td>
|
||||
<Link to={sceneLink}>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="image-thumbnail"
|
||||
alt={title}
|
||||
src={scene.paths.screenshot ?? ""}
|
||||
|
||||
@@ -686,6 +686,16 @@ input[type="range"].blue-slider {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.hover-scrubber-inactive {
|
||||
.hover-scrubber-area {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.hover-scrubber-indicator {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.hover-scrubber-indicator {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
bottom: -100%;
|
||||
|
||||
@@ -85,6 +85,7 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
|
||||
if (props.interactiveHeatmap) {
|
||||
return (
|
||||
<img
|
||||
loading="lazy"
|
||||
src={props.interactiveHeatmap}
|
||||
alt="interactive heatmap"
|
||||
className="interactive-heatmap"
|
||||
|
||||
@@ -160,6 +160,7 @@ export const StudioCard: React.FC<IProps> = ({
|
||||
linkClassName="studio-card-header"
|
||||
image={
|
||||
<img
|
||||
loading="lazy"
|
||||
className="studio-card-image"
|
||||
alt={studio.name}
|
||||
src={studio.image_path ?? ""}
|
||||
|
||||
@@ -49,6 +49,7 @@ const TaggerSceneDetails: React.FC<ITaggerSceneDetails> = ({ scene }) => {
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
|
||||
@@ -102,7 +102,12 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
key={p.remote_site_id}
|
||||
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>
|
||||
</Button>
|
||||
));
|
||||
|
||||
@@ -611,7 +611,7 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
|
||||
<div></div>
|
||||
<div>
|
||||
<Card className="studio-card">
|
||||
<img src={studio.image_path ?? ""} alt="" />
|
||||
<img loading="lazy" src={studio.image_path ?? ""} alt="" />
|
||||
</Card>
|
||||
</div>
|
||||
<div className={`${CLASSNAME}-details-text`}>
|
||||
|
||||
@@ -187,6 +187,7 @@ export const TagCard: React.FC<IProps> = ({
|
||||
linkClassName="tag-card-header"
|
||||
image={
|
||||
<img
|
||||
loading="lazy"
|
||||
className="tag-card-image"
|
||||
alt={tag.name}
|
||||
src={tag.image_path ?? ""}
|
||||
|
||||
@@ -66,6 +66,7 @@ const Preview: React.FC<{
|
||||
|
||||
const image = (
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
className="wall-item-media"
|
||||
src={
|
||||
|
||||
Reference in New Issue
Block a user