improve: optimize timeline scrolling
This commit is contained in:
parent
f778c4f44b
commit
d4f14b97b0
@ -45,15 +45,84 @@ export default function RewindPage() {
|
|||||||
const [currentFrameId, setCurrentFrameId] = useState<number | null>(null);
|
const [currentFrameId, setCurrentFrameId] = useState<number | null>(null);
|
||||||
const [images, setImages] = useState<Record<number, string>>({});
|
const [images, setImages] = useState<Record<number, string>>({});
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const lastRequestTime = useRef(Date.now());
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const lastAvaliableFrameId = useRef<number | null>(null);
|
const lastAvaliableFrameId = useRef<number | null>(null);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const updatedTimes = useRef<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadingQueue = useRef<number[]>([]);
|
||||||
if (currentFrameId && images[currentFrameId]) {
|
const isProcessingQueue = useRef(false);
|
||||||
lastAvaliableFrameId.current = currentFrameId;
|
|
||||||
|
const processQueue = useCallback(async () => {
|
||||||
|
if (!port || isProcessingQueue.current || loadingQueue.current.length === 0) return;
|
||||||
|
|
||||||
|
isProcessingQueue.current = true;
|
||||||
|
const frameId = loadingQueue.current.shift()!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startUpdateTimes = updatedTimes.current;
|
||||||
|
const response = await fetch(`http://localhost:${port}/frame/${frameId}`, {
|
||||||
|
headers: {
|
||||||
|
"x-api-key": apiKey
|
||||||
}
|
}
|
||||||
}, [images, currentFrameId]);
|
});
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setImages((prev) => {
|
||||||
|
const newImages = { ...prev, [frameId]: url };
|
||||||
|
if (updatedTimes.current <= startUpdateTimes) {
|
||||||
|
lastAvaliableFrameId.current = frameId;
|
||||||
|
updatedTimes.current++;
|
||||||
|
}
|
||||||
|
return newImages;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isProcessingQueue.current = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
processQueue();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [apiKey, port]);
|
||||||
|
|
||||||
|
const loadImage = useCallback(
|
||||||
|
(frameId: number) => {
|
||||||
|
if (!port || images[frameId]) return;
|
||||||
|
|
||||||
|
// Add to queue if not already in it
|
||||||
|
if (!loadingQueue.current.includes(frameId)) {
|
||||||
|
loadingQueue.current.push(frameId);
|
||||||
|
// preserve up to 5 tasks in the queue
|
||||||
|
loadingQueue.current = loadingQueue.current.slice(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing if not already running
|
||||||
|
if (!isProcessingQueue.current) {
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[images, port, processQueue]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load current frame after 400ms of inactivity
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFrameId) {
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
loadImage(currentFrameId);
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [currentFrameId, loadImage]);
|
||||||
|
|
||||||
// Fetch timeline data
|
// Fetch timeline data
|
||||||
const fetchTimeline = useCallback(
|
const fetchTimeline = useCallback(
|
||||||
@ -83,29 +152,6 @@ export default function RewindPage() {
|
|||||||
fetchTimeline();
|
fetchTimeline();
|
||||||
}, [fetchTimeline]);
|
}, [fetchTimeline]);
|
||||||
|
|
||||||
const loadImage = useCallback(
|
|
||||||
(frameId: number) => {
|
|
||||||
if (!port) return;
|
|
||||||
// Rate limit to at most 1 request every 200ms
|
|
||||||
const now = Date.now();
|
|
||||||
if (images[frameId]) return;
|
|
||||||
|
|
||||||
lastRequestTime.current = now;
|
|
||||||
fetch(`http://localhost:${port}/frame/${frameId}`, {
|
|
||||||
headers: {
|
|
||||||
"x-api-key": apiKey
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => res.blob())
|
|
||||||
.then((blob) => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
setImages((prev) => ({ ...prev, [frameId]: url }));
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
},
|
|
||||||
[apiKey, images, port]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load initial images
|
// Load initial images
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timeline.length > 0 && !currentFrameId) {
|
if (timeline.length > 0 && !currentFrameId) {
|
||||||
@ -134,6 +180,8 @@ export default function RewindPage() {
|
|||||||
const newIndex = Math.min(Math.max(currentIndex - delta, 0), timeline.length - 1);
|
const newIndex = Math.min(Math.max(currentIndex - delta, 0), timeline.length - 1);
|
||||||
const newFrameId = timeline[newIndex].id;
|
const newFrameId = timeline[newIndex].id;
|
||||||
|
|
||||||
|
console.log(currentFrameId, lastAvaliableFrameId);
|
||||||
|
|
||||||
if (newFrameId !== currentFrameId) {
|
if (newFrameId !== currentFrameId) {
|
||||||
setCurrentFrameId(newFrameId);
|
setCurrentFrameId(newFrameId);
|
||||||
// Preload adjacent images
|
// Preload adjacent images
|
||||||
@ -178,7 +226,7 @@ export default function RewindPage() {
|
|||||||
|
|
||||||
{/* Time capsule */}
|
{/* Time capsule */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-8 left-8 bg-zinc-800 bg-opacity-80 backdrop-blur-lg
|
className="absolute bottom-8 left-8 bg-zinc-800 text-white bg-opacity-80 backdrop-blur-lg
|
||||||
rounded-full px-4 py-2 text-xl"
|
rounded-full px-4 py-2 text-xl"
|
||||||
>
|
>
|
||||||
{currentFrameId
|
{currentFrameId
|
||||||
@ -205,7 +253,7 @@ export default function RewindPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="mt-2 text-base text-zinc-400">#{frame.id}</span>
|
<span className="mt-2 text-base text-zinc-400">#{frame.id}</span>
|
||||||
<span>
|
<span className="text-white">
|
||||||
{dayjs().diff(dayjs.unix(frame.createdAt), "second")} sec ago
|
{dayjs().diff(dayjs.unix(frame.createdAt), "second")} sec ago
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,6 +120,9 @@ app.get("/frame/:id", async (c) => {
|
|||||||
|
|
||||||
if (existsSync(decodedPath)) {
|
if (existsSync(decodedPath)) {
|
||||||
const imageBuffer = fs.readFileSync(decodedPath);
|
const imageBuffer = fs.readFileSync(decodedPath);
|
||||||
|
setTimeout(() => {
|
||||||
|
fs.unlinkSync(decodedPath);
|
||||||
|
}, 1000);
|
||||||
return new Response(imageBuffer, {
|
return new Response(imageBuffer, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
Loading…
Reference in New Issue
Block a user