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.
95 lines
2.7 KiB
95 lines
2.7 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 = 300;
|
|
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);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Generate thumbnail
|
|
const promise = generateThumbnail(fullSrc).then((thumb) => {
|
|
setThumbnail(fullSrc, thumb);
|
|
removePending(fullSrc);
|
|
return thumb;
|
|
});
|
|
setPending(fullSrc, promise);
|
|
|
|
promise.then((thumb) => {
|
|
if (prevSrcRef.current === fullSrc) {
|
|
setThumbnailSrc(thumb);
|
|
}
|
|
});
|
|
}, [fullSrc]);
|
|
|
|
if (!fullSrc) return null;
|
|
|
|
return shouldUseThumbnail && thumbnailSrc ? thumbnailSrc : fullSrc;
|
|
}
|
|
|