cloudy9101_obsidian-image-i.../ImagesModal.tsx
2025-09-16 22:46:44 +12:00

312 lines
12 KiB
TypeScript

import * as React from "react";
import { getFetcher } from "fetchers";
import {
Fetcher,
Image,
imageProviders,
imageSizes,
imageSizesMapping,
providerMapping,
} from "fetchers/constants";
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
import { Notice } from "obsidian";
import { debounce } from "./utils";
import Loading from "./Loading";
import NoResult from "./NoResult";
import { ImageProvider, ImageSize, PluginSettings } from "SettingTab";
interface Props {
fetcher: Fetcher;
onFetcherChange: (fetcher: Fetcher) => void;
settings: PluginSettings;
random: boolean;
onSelect: (image: Image) => void;
}
const ImagesModal = ({
fetcher: defaultFetcher,
onFetcherChange,
settings,
random,
onSelect,
}: Props) => {
const [fetcher, setFetcher] = useState(defaultFetcher);
const [query, setQuery] = useState("");
const [images, setImages] = useState<Image[]>([]);
const ref = useRef<HTMLDivElement | null>(null);
const selectedImageRef = useRef<HTMLDivElement | null>(null);
const [selectedImage, setSelectedImage] = useState(0);
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
const filteredImageProviders = random ? [ImageProvider.unsplash] : imageProviders
const fetchImages = async (query: string) => {
try {
const fetchFunc = random ? fetcher.randomImage : fetcher.searchImages
const images = await fetchFunc(query);
setImages(images);
setSelectedImage(0);
} catch (e) {
setError(e.message);
console.error(e);
new Notice("Something went wrong, please contact the plugin author.");
}
setLoading(false);
};
const debouncedFetchImages = useCallback(debounce(fetchImages, 1000), [
fetcher,
]);
const onQueryChange = async (query: string) => {
setQuery(query);
debouncedFetchImages(query);
};
const onInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
setLoading(true);
setError(undefined);
onQueryChange(e.target.value);
};
const onProviderChange = async (provider: ImageProvider) => {
setLoading(true);
setError(undefined);
const newFetcher = getFetcher(
{ ...settings, imageProvider: provider, imageSize: fetcher.imageSize },
fetcher.vault,
);
setFetcher(newFetcher);
onFetcherChange(newFetcher);
};
const onProviderSelectorChange = async (
e: ChangeEvent<HTMLSelectElement>,
) => {
const provider = e.target.value as ImageProvider;
await onProviderChange(provider);
};
const onImageSizeChange = async (quality: ImageSize) => {
setLoading(true);
setError(undefined);
const newFetcher = getFetcher(
{
...settings,
imageSize: quality,
imageProvider: fetcher.imageProvider,
},
fetcher.vault,
);
setFetcher(newFetcher);
onFetcherChange(newFetcher);
};
const onImageSizeSelectorChange = async (
e: ChangeEvent<HTMLSelectElement>,
) => {
const quality = e.target.value as ImageSize;
await onImageSizeChange(quality);
};
const onPrevBtnClick = () => {
setLoading(true);
fetcher.prevPage();
fetchImages(query);
};
const onNextBtnClick = () => {
setLoading(true);
fetcher.nextPage();
fetchImages(query);
};
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.ctrlKey && e.key === "n") {
setSelectedImage((prev) => (prev + 1 >= images.length ? 0 : prev + 1));
} else if (e.ctrlKey && e.key === "p") {
setSelectedImage((prev) =>
prev - 1 < 0 ? images.length - 1 : prev - 1,
);
} else if (e.ctrlKey && e.key === "u") {
let index = filteredImageProviders.indexOf(fetcher.imageProvider) + 1;
if (index >= filteredImageProviders.length) {
index = 0;
}
onProviderChange(filteredImageProviders[index]);
} else if (e.ctrlKey && e.key === "i") {
let index = imageSizes.indexOf(fetcher.imageSize) + 1;
if (index >= imageSizes.length) {
index = 0;
}
onImageSizeChange(imageSizes[index]);
} else if (e.key === "Enter") {
e.preventDefault();
onSelect(images[selectedImage]);
}
},
[images, selectedImage, fetcher.imageProvider],
);
useEffect(() => {
const element = ref.current;
if (element) {
element.addEventListener("keydown", handleKeyDown);
}
return () => {
const element = ref.current;
if (element) {
element.removeEventListener("keydown", handleKeyDown);
}
};
}, [handleKeyDown, ref.current]);
useEffect(() => {
if (selectedImageRef.current) {
selectedImageRef.current.scrollIntoView({ block: "nearest" });
}
}, [selectedImageRef.current]);
useEffect(() => {
if (fetcher) {
onQueryChange(query);
}
}, [fetcher]);
const hasPagination = fetcher.hasPrevPage() || fetcher.hasNextPage();
return (
<div ref={ref} className="container">
<div className="input-group">
<input
autoFocus={true}
value={query}
onChange={onInputChange}
className="query-input"
/>
<select
value={fetcher.imageProvider}
onChange={onProviderSelectorChange}
className="selector"
>
{filteredImageProviders.map((provider) => (
<option key={provider} value={provider}>
{providerMapping[provider]}
</option>
))}
</select>
<select
value={fetcher.imageSize}
onChange={onImageSizeSelectorChange}
className="selector"
>
{imageSizes.map((size) => (
<option key={size} value={size}>
{imageSizesMapping[size]}
</option>
))}
</select>
</div>
{loading && <Loading />}
{query !== "" && !loading && fetcher.noResult() && <NoResult />}
{error}
<div className={`scroll-area${loading ? " loading" : ""}`}>
<div className="images-list">
{images.map((image, index) => (
<div
onClick={() => onSelect(image)}
onMouseMove={() => setSelectedImage(index)}
key={image.url}
className={`query-result${
selectedImage === index ? " is-selected" : ""
}`}
ref={index === selectedImage ? selectedImageRef : null}
>
<img key={image.url} src={image.thumb} />
</div>
))}
</div>
{hasPagination && (
<div className="pagination">
<div>
{fetcher.hasPrevPage() && (
<button
className="btn"
disabled={loading}
tabIndex={-1}
onClick={onPrevBtnClick}
>
PREV
</button>
)}
</div>
<div>
{fetcher.hasNextPage() && (
<button
className="btn"
disabled={loading}
tabIndex={-1}
onClick={onNextBtnClick}
>
NEXT
</button>
)}
</div>
</div>
)}
{fetcher.imageProvider === ImageProvider.pixabay && (
<div className="pixabay-logo">
<a href="https://pixabay.com/" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
height="100"
width="100"
version="1.1"
viewBox="0 0 500 500"
>
<g transform="translate(-507.42999,175.07001)">
<rect
height="500"
width="500"
y="-175.07"
x="507.43"
fill="#222"
/>
<g
fill="#FFF"
transform="matrix(2.7777778,0,0,2.7777778,-2096.4989,0.29221019)"
>
<path d="m993.33-7.7954c-2.671,0-4.8365-2.1665-4.8365-4.8392h-16.569c0,2.6727-2.1651,4.8392-4.8363,4.8392h-4.4254v51.484h84.798v-51.483h-54.065zm-4.83,15.24h-16.571v-7.2076h16.569v7.2076zm29.124,28.522c-9.8169,0-17.804-7.9906-17.804-17.813,0-9.8226,7.987-17.402,17.804-17.402,9.8171,0,17.804,7.5795,17.804,17.402,0,9.8226-7.9865,17.813-17.804,17.813zm9.571-17.813c0,5.28-4.2937,9.576-9.5707,9.576-5.2772,0-9.5708-4.2963-9.5708-9.576,0-5.2806,4.2936-9.576,9.5708-9.576,5.277,0,9.5707,4.2957,9.5707,9.576zm66.262-29.941-21.421,55.346-17.241-6.6808v-9.4327l12.218,4.7345,15.073-38.942-56.483-21.887-5.2351,13.525h-9.426l9.6383-24.901,72.876,28.239z" />
<g
fontFamily="VomZom"
fontSize="27.43096924px"
line-height="125%"
fontStretch="normal"
fontVariant="normal"
fontWeight="normal"
fontStyle="normal"
>
<path d="m972.14,64.921c-2.7039,0.06735-4.9506,0.99582-6.7402,2.7854s-2.7181,4.0364-2.7854,6.7402v17.223h3.7847v-7.6545h5.7409c2.7056-0.0691,4.9595-1.0047,6.7615-2.8067s2.7375-4.0558,2.8067-6.7615c-0.0691-2.7039-1.0047-4.9506-2.8067-6.7402s-4.0558-2.718-6.7615-2.7854zm-5.7409,15.309,0-5.7834c0.0399-1.6346,0.59801-2.99,1.6744-4.0665,1.0764-1.0764,2.4319-1.6345,4.0665-1.6744,1.6549,0.03988,3.0228,0.59802,4.1037,1.6744,1.0808,1.0764,1.6407,2.4319,1.6797,4.0665-0.039,1.6549-0.59891,3.0228-1.6797,4.1037-1.0809,1.0809-2.4488,1.6408-4.1037,1.6797z" />
<path d="m984.13,83.93,3.7422,0,0-19.094-3.7422,0z" />
<path d="m1004.9,83.973-4.9073-6.2277-4.4076,6.2277-5.282,0,7.3341-9.7788-6.7452-9.3327,5.1214,0,4.0329,5.8352,4.4968-5.8352,5.0857,0-7.3341,9.3327,8.0122,9.7788z" />
<path d="m1020,64.836c-2.78,0.01392-5.5411,1.2356-7.3217,3.389-2.5173,2.889-2.9277,7.3319-1.1016,10.68,1.503,2.8167,4.4818,4.7722,7.6742,4.9822,2.0115,0.08973,4.028,0.02048,6.0416,0.04228h4.2331c-0.01-3.315,0.02-6.6309-0.015-9.9454-0.098-2.8431-1.5228-5.5984-3.8252-7.2788-1.6236-1.2335-3.6602-1.8331-5.6853-1.8696zm0,15.309c-2.4155,0.03477-4.7585-1.5866-5.4781-3.9086-0.8301-2.3925,0.086-5.2767,2.2388-6.6514,2.3653-1.6093,5.9042-1.1463,7.6851,1.1269,1.09,1.2527,1.3722,2.9584,1.2951,4.5678v4.865h-5.7z" />
<path d="m1035.6,64.921v-7.6545h-3.7848c0.01,5.8108-0.012,11.622,0.01,17.432,0.054,3.9898,2.8702,7.7511,6.7059,8.8726,3.6906,1.1933,8.0347-0.12452,10.369-3.2412,2.8592-3.5647,2.6169-9.1977-0.6105-12.455-1.9534-2.1419-4.9109-3.0977-7.76-2.9545h-4.9289zm5.7834,15.309c-2.6131,0.04941-5.1132-1.8749-5.6269-4.4552-0.2827-1.5643-0.1078-3.1645-0.1565-4.7461v-2.323c2.1274,0.02223,4.2582-0.04689,6.3833,0.03925,2.6883,0.20268,4.9782,2.5587,5.1161,5.2493,0.2744,2.6972-1.6024,5.4384-4.2654,6.0466-0.4742,0.1185-0.9623,0.1776-1.4506,0.18916z" />
<path d="m1062.7,64.836c-2.7799,0.01394-5.5411,1.2356-7.3217,3.389-2.5172,2.889-2.9276,7.3319-1.1015,10.68,1.5029,2.8167,4.4818,4.7722,7.6742,4.9822,2.0115,0.08973,4.0279,0.02048,6.0415,0.04228h4.2332c-0.01-3.315,0.02-6.6309-0.015-9.9454-0.098-2.8431-1.5228-5.5984-3.8252-7.2788-1.6236-1.2335-3.6602-1.8331-5.6854-1.8696zm0,15.309c-2.4155,0.03475-4.7585-1.5866-5.478-3.9086-0.8301-2.3925,0.086-5.2767,2.2388-6.6514,2.3653-1.6093,5.9042-1.1463,7.6851,1.1269,1.09,1.2527,1.3721,2.9584,1.295,4.5678v4.865h-5.7z" />
<path d="m1089.9,64.879c-0.016,3.3276,0.032,6.6571-0.025,9.9835-0.1308,2.7-2.413,5.0836-5.1161,5.2865-2.7002,0.34612-5.4922-1.4815-6.1585-4.1395-0.3846-1.5788-0.1677-3.2172-0.2248-4.8256v-6.305h-3.7848c0.014,3.3424-0.029,6.6862,0.023,10.028,0.1465,4.1428,3.3074,7.9466,7.3694,8.8022,2.7164,0.66144,5.7202,0.05068,7.9166-1.6922,0.066,2.6174-1.8415,5.1442-4.428,5.6694-1.0594,0.24194-2.1504,0.13118-3.2266,0.15651v3.7847c2.1224,0.04087,4.3304-0.0016,6.2465-1.0395,3.2212-1.6055,5.2812-5.14,5.1928-8.7259v-16.983c-1.2616,0.000002-2.5231,0.000003-3.7847,0.000005z" />
</g>
</g>
</g>
</svg>
</a>
</div>
)}
</div>
</div>
);
};
export { ImagesModal };