mirror of
https://github.com/lumehq/lume.git
synced 2025-03-17 21:32:32 +01:00
Add lazy carousel (#214)
* refactor: carousel * feat: improve image carousel
This commit is contained in:
parent
e4a317f038
commit
4f0f210076
Binary file not shown.
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 17 KiB |
@ -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 { 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[] }) {
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
const [slidesInView, setSlidesInView] = useState([]);
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
dragFree: true,
|
||||
align: "start",
|
||||
watchSlides: false,
|
||||
});
|
||||
|
||||
const imageUrls = useMemo(() => {
|
||||
if (settings.image_resize_service.length) {
|
||||
const newUrls = urls.map(
|
||||
(url) =>
|
||||
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
|
||||
);
|
||||
let newUrls: string[];
|
||||
|
||||
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;
|
||||
} else {
|
||||
return urls;
|
||||
}
|
||||
}, [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) {
|
||||
return (
|
||||
<div className="px-3 group">
|
||||
@ -40,26 +96,84 @@ export function Images({ urls }: { urls: string[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
items={imageUrls}
|
||||
renderItem={({ item, index, isSnapPoint }) => (
|
||||
<CarouselItem key={item + index} isSnapPoint={isSnapPoint}>
|
||||
<img
|
||||
src={item}
|
||||
alt={item}
|
||||
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(item)}
|
||||
onKeyDown={() => open(item)}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
currentTarget.src = "/404.jpg";
|
||||
}}
|
||||
/>
|
||||
</CarouselItem>
|
||||
)}
|
||||
/>
|
||||
<div className="relative pl-2 overflow-hidden group">
|
||||
<div ref={emblaRef} className="w-full">
|
||||
<div className="flex w-full gap-2 scrollbar-none">
|
||||
{imageUrls.map((url, index) => (
|
||||
<LazyImage
|
||||
/* biome-ignore lint/suspicious/noArrayIndexKey: url can be duplicated */
|
||||
key={url + index}
|
||||
url={url}
|
||||
inView={slidesInView.indexOf(index) > -1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
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
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user