Add lazy carousel (#214)

* refactor: carousel

* feat: improve image carousel
This commit is contained in:
雨宮蓮 2024-06-21 07:51:11 +07:00 committed by GitHub
parent e4a317f038
commit 4f0f210076
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 141 additions and 27 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,23 +1,79 @@
import { Carousel, CarouselItem } from "@lume/ui"; import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react"; import useEmblaCarousel from "embla-carousel-react";
import { useCallback, useEffect, useMemo, useState } from "react";
export function Images({ urls }: { urls: string[] }) { export function Images({ urls }: { urls: string[] }) {
const { settings } = useRouteContext({ strict: false }); const { settings } = useRouteContext({ strict: false });
const [slidesInView, setSlidesInView] = useState([]);
const [emblaRef, emblaApi] = useEmblaCarousel({
dragFree: true,
align: "start",
watchSlides: false,
});
const imageUrls = useMemo(() => { const imageUrls = useMemo(() => {
if (settings.image_resize_service.length) { if (settings.image_resize_service.length) {
const newUrls = urls.map( let newUrls: string[];
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`, if (urls.length === 1) {
); newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
);
} else {
newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
);
}
return newUrls; return newUrls;
} else { } else {
return urls; return urls;
} }
}, [settings.image_resize_service]); }, [settings.image_resize_service]);
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev();
}, [emblaApi]);
const scrollNext = useCallback(() => {
if (emblaApi) emblaApi.scrollNext();
}, [emblaApi]);
const updateSlidesInView = useCallback((emblaApi) => {
setSlidesInView((slidesInView) => {
if (slidesInView.length === emblaApi.slideNodes().length) {
emblaApi.off("slidesInView", updateSlidesInView);
}
const inView = emblaApi
.slidesInView()
.filter((index) => !slidesInView.includes(index));
return slidesInView.concat(inView);
});
}, []);
useEffect(() => {
if (emblaApi && urls.length > 1) {
updateSlidesInView(emblaApi);
emblaApi.on("slidesInView", updateSlidesInView);
emblaApi.on("reInit", updateSlidesInView);
}
return () => {
emblaApi?.off("slidesInView", updateSlidesInView);
emblaApi?.off("reInit", updateSlidesInView);
};
}, [emblaApi, updateSlidesInView]);
if (urls.length === 1) { if (urls.length === 1) {
return ( return (
<div className="px-3 group"> <div className="px-3 group">
@ -40,26 +96,84 @@ export function Images({ urls }: { urls: string[] }) {
} }
return ( return (
<Carousel <div className="relative pl-2 overflow-hidden group">
items={imageUrls} <div ref={emblaRef} className="w-full">
renderItem={({ item, index, isSnapPoint }) => ( <div className="flex w-full gap-2 scrollbar-none">
<CarouselItem key={item + index} isSnapPoint={isSnapPoint}> {imageUrls.map((url, index) => (
<img <LazyImage
src={item} /* biome-ignore lint/suspicious/noArrayIndexKey: url can be duplicated */
alt={item} key={url + index}
loading="lazy" url={url}
decoding="async" inView={slidesInView.indexOf(index) > -1}
style={{ contentVisibility: "auto" }} />
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15" ))}
onClick={() => open(item)} </div>
onKeyDown={() => open(item)} </div>
onError={({ currentTarget }) => { <div
currentTarget.onerror = null; aria-hidden
currentTarget.src = "/404.jpg"; className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
}} >
/> <button
</CarouselItem> type="button"
)} disabled={!emblaApi?.canScrollPrev}
/> className={cn(
"size-11 rounded-full bg-black/30 backdrop-blur-sm flex items-center justify-center text-white",
!emblaApi?.canScrollPrev ? "opacity-50" : "",
)}
onClick={() => scrollPrev()}
>
<ArrowLeftIcon className="size-6" />
</button>
<button
type="button"
disabled={!emblaApi?.canScrollNext}
className={cn(
"size-11 rounded-full bg-black/30 backdrop-blur-sm flex items-center justify-center text-white",
!emblaApi?.canScrollNext ? "opacity-50" : "",
)}
onClick={() => scrollNext()}
>
<ArrowRightIcon className="size-6" />
</button>
</div>
</div>
);
}
function LazyImage({ url, inView }: { url: string; inView: boolean }) {
const [hasLoaded, setHasLoaded] = useState(false);
const setLoaded = useCallback(() => {
if (inView) setHasLoaded(true);
}, [inView, setHasLoaded]);
return (
<div className="w-[240px] h-[320px] shrink-0 relative rounded-lg overflow-hidden">
{!hasLoaded ? (
<div className="flex items-center justify-center size-full bg-black/5 dark:bg-white/5">
<Spinner className="size-4" />
</div>
) : null}
<img
src={
inView
? url
: "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
}
data-src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onKeyDown={() => open(url)}
onLoad={setLoaded}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
); );
} }