You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

100 lines
2.8 KiB

import { useState, useEffect, useRef, useCallback } from "react";
import { useStore, type ReactFlowState } from "@xyflow/react";
import { generateThumbnail } from "@/utils/imageThumbnail";
import {
getThumbnail,
setThumbnail,
getPending,
setPending,
removePending,
} from "@/store/thumbnailCache";
const EFFECTIVE_WIDTH_THRESHOLD = 200;
const DEFAULT_NODE_WIDTH = 350;
/**
* Returns an adaptive image source that swaps between a small JPEG thumbnail
* and the full-resolution image based on how large the node appears on screen.
*
* - When `nodeWidth * zoom < 300px` → returns thumbnail (~256px JPEG)
* - Otherwise → returns full source
* - Thumbnails are generated eagerly when fullSrc changes and cached in memory
*/
export function useAdaptiveImageSrc(
fullSrc: string | null | undefined,
nodeId: string
): string | null {
// Read zoom (quantized to 0.1 steps) and node width in a single selector
// to minimize re-renders
const shouldUseThumbnail = useStore(
useCallback(
(state: ReactFlowState) => {
const zoom = state.transform[2];
const node = state.nodeLookup.get(nodeId);
const width =
node?.measured?.width ?? (node as Record<string, unknown>)?.width as number ?? DEFAULT_NODE_WIDTH;
return (width as number) * zoom < EFFECTIVE_WIDTH_THRESHOLD;
},
[nodeId]
),
// Only re-render when the boolean result changes
(a, b) => a === b
);
const [thumbnailSrc, setThumbnailSrc] = useState<string | null>(null);
const prevSrcRef = useRef<string | null>(null);
// Eagerly generate thumbnail when fullSrc changes
useEffect(() => {
if (!fullSrc) {
setThumbnailSrc(null);
prevSrcRef.current = null;
return;
}
// Skip if same source
if (fullSrc === prevSrcRef.current) return;
prevSrcRef.current = fullSrc;
// Check cache first
const cached = getThumbnail(fullSrc);
if (cached) {
setThumbnailSrc(cached);
return;
}
// Check if generation is already in progress
const existing = getPending(fullSrc);
if (existing) {
existing
.then((thumb) => {
if (prevSrcRef.current === fullSrc) {
setThumbnailSrc(thumb);
}
})
.catch(() => {});
return;
}
// Generate thumbnail
const promise = generateThumbnail(fullSrc)
.then((thumb) => {
setThumbnail(fullSrc, thumb);
if (prevSrcRef.current === fullSrc) {
setThumbnailSrc(thumb);
}
return thumb;
})
.finally(() => {
removePending(fullSrc);
});
setPending(fullSrc, promise);
// Suppress unhandled rejection — fullSrc falls back gracefully
promise.catch(() => {});
}, [fullSrc]);
if (!fullSrc) return null;
return shouldUseThumbnail && thumbnailSrc ? thumbnailSrc : fullSrc;
}