115 lines
3.8 KiB
JavaScript
115 lines
3.8 KiB
JavaScript
import { useEffect, useMemo, useState, useCallback } from "react";
|
|
import axios from "axios";
|
|
import { useTranslation } from "react-i18next";
|
|
import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
|
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
|
|
|
function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState, context = "chat" }) {
|
|
const [galleryImages, setgalleryImages] = externalMediaState;
|
|
const [rawMedia, setRawMedia] = useState([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { t } = useTranslation();
|
|
|
|
const fetchThumbnails = useCallback(async () => {
|
|
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
|
|
return result.data;
|
|
}, [jobId]);
|
|
|
|
useEffect(() => {
|
|
if (!jobId) return;
|
|
setIsLoading(true);
|
|
fetchThumbnails()
|
|
.then(setRawMedia)
|
|
.catch(console.error)
|
|
.finally(() => setIsLoading(false));
|
|
}, [jobId, fetchThumbnails]);
|
|
|
|
const documents = useMemo(() => {
|
|
return rawMedia
|
|
.filter((v) => v.type?.startsWith("image"))
|
|
.map((v) => ({
|
|
src: v.thumbnailUrl,
|
|
thumbnail: v.thumbnailUrl,
|
|
fullsize: v.originalUrl,
|
|
width: 225,
|
|
height: 225,
|
|
thumbnailWidth: 225,
|
|
thumbnailHeight: 225,
|
|
caption: v.key,
|
|
filename: v.key,
|
|
// additional properties if needed
|
|
key: v.key,
|
|
id: v.id,
|
|
type: v.type,
|
|
size: v.size,
|
|
extension: v.extension
|
|
}));
|
|
}, [rawMedia]);
|
|
|
|
useEffect(() => {
|
|
const prevSelection = new Map(galleryImages.map((p) => [p.filename, p.isSelected]));
|
|
const nextImages = documents.map((d) => ({ ...d, isSelected: prevSelection.get(d.filename) || false }));
|
|
// Micro-optimization: if array length and each filename + selection flag match, skip creating a new array.
|
|
if (galleryImages.length === nextImages.length) {
|
|
let identical = true;
|
|
for (let i = 0; i < nextImages.length; i++) {
|
|
if (
|
|
galleryImages[i].filename !== nextImages[i].filename ||
|
|
galleryImages[i].isSelected !== nextImages[i].isSelected
|
|
) {
|
|
identical = false;
|
|
break;
|
|
}
|
|
}
|
|
if (identical) {
|
|
setIsLoading(false); // ensure loading stops even on no-change
|
|
return;
|
|
}
|
|
}
|
|
setgalleryImages(nextImages);
|
|
setIsLoading(false); // stop loading after transform regardless of emptiness
|
|
}, [documents, setgalleryImages, galleryImages, jobId]);
|
|
|
|
const handleToggle = useCallback(
|
|
(idx) => {
|
|
setgalleryImages((imgs) => imgs.map((g, gIdx) => (gIdx === idx ? { ...g, isSelected: !g.isSelected } : g)));
|
|
},
|
|
[setgalleryImages]
|
|
);
|
|
|
|
const messageStyle = { textAlign: "center", padding: "1rem" };
|
|
|
|
if (!jobId) {
|
|
return (
|
|
<div aria-label="media gallery unavailable" style={{ position: "relative", minHeight: 80 }}>
|
|
<div style={messageStyle}>No job selected.</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="clearfix"
|
|
style={{ position: "relative", minHeight: 80 }}
|
|
data-jobid={jobId}
|
|
aria-label={`media gallery for job ${jobId}`}
|
|
>
|
|
{isLoading && galleryImages.length === 0 && (
|
|
<div className="local-gallery-loading" style={messageStyle} role="status" aria-live="polite">
|
|
<LoadingSpinner />
|
|
</div>
|
|
)}
|
|
{galleryImages.length > 0 && (
|
|
<LocalMediaGrid images={galleryImages} minColumns={4} context={context} onToggle={handleToggle} />
|
|
)}
|
|
{galleryImages.length > 0 && (
|
|
<div style={{ fontSize: 10, color: "#888", marginTop: 4 }} aria-live="off">
|
|
{`${t("general.labels.media")}: ${galleryImages.length}`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default JobsDocumentImgproxyGalleryExternal;
|