182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
import { cn } from "@/commons";
|
|
import { Spinner } from "@/components";
|
|
import { settingsQueryOptions } from "@/routes/__root";
|
|
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
|
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
import { open } from "@tauri-apps/plugin-shell";
|
|
import useEmblaCarousel from "embla-carousel-react";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
export function Images({ urls }: { urls: string[] }) {
|
|
const [slidesInView, setSlidesInView] = useState<number[]>([]);
|
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
dragFree: true,
|
|
align: "start",
|
|
watchSlides: false,
|
|
});
|
|
|
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
|
|
|
const imageUrls = useMemo(() => {
|
|
if (settings.data.resize_service) {
|
|
let newUrls: string[];
|
|
|
|
if (urls.length === 1) {
|
|
newUrls = urls.map((url) => {
|
|
if (url.includes("_next/")) {
|
|
return url;
|
|
}
|
|
if (url.includes("bsky.network")) {
|
|
return url;
|
|
}
|
|
return `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
|
});
|
|
} else {
|
|
newUrls = urls.map((url) => {
|
|
if (url.includes("_next/")) {
|
|
return url;
|
|
}
|
|
if (url.includes("bsky.network")) {
|
|
return url;
|
|
}
|
|
return `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
|
});
|
|
}
|
|
|
|
return newUrls;
|
|
} else {
|
|
return urls;
|
|
}
|
|
}, [settings.data.resize_service]);
|
|
|
|
const scrollPrev = useCallback(() => {
|
|
if (emblaApi) emblaApi.scrollPrev();
|
|
}, [emblaApi]);
|
|
|
|
const scrollNext = useCallback(() => {
|
|
if (emblaApi) emblaApi.scrollNext();
|
|
}, [emblaApi]);
|
|
|
|
const updateSlidesInView = useCallback(() => {
|
|
setSlidesInView((slidesInView) => {
|
|
if (slidesInView.length === emblaApi?.slideNodes().length) {
|
|
emblaApi?.off("slidesInView", updateSlidesInView);
|
|
}
|
|
|
|
const inView = emblaApi
|
|
?.slidesInView()
|
|
.filter((index) => !slidesInView.includes(index));
|
|
|
|
if (inView) {
|
|
return slidesInView.concat(inView);
|
|
} else {
|
|
return slidesInView;
|
|
}
|
|
});
|
|
}, [emblaApi]);
|
|
|
|
useEffect(() => {
|
|
if (emblaApi && urls.length > 1) {
|
|
updateSlidesInView();
|
|
|
|
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">
|
|
<img
|
|
src={imageUrls[0]}
|
|
alt={urls[0]}
|
|
decoding="async"
|
|
style={{ contentVisibility: "auto" }}
|
|
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
|
|
onClick={() => urls[0]}
|
|
onKeyDown={() => urls[0]}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="relative px-3 overflow-hidden group">
|
|
<div ref={emblaRef} className="w-full h-[320px]">
|
|
<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 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()}
|
|
>
|
|
<ArrowLeft 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()}
|
|
>
|
|
<ArrowRight 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}
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|