Skip to content

Commit 134eeb6

Browse files
JPPhotolsteinclaude
authored
Fix progress preview gallery selection (invoke-ai#9217)
* Fix progress preview gallery selection * Remove redundant clearTimeout in unmount cleanup effect The reveal effect's own cleanup already clears selectedImageRevealTimeoutId on unmount; the unmount-only effect just needs to reset the atom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 75f1992 commit 134eeb6

3 files changed

Lines changed: 97 additions & 10 deletions

File tree

invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
3030
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
3131
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
3232
const { goToPreviousImage, goToNextImage, isFetching } = useNextPrevItemNavigation();
33-
const { onLoadImage, $progressEvent, $progressImage } = useImageViewerContext();
33+
const { onLoadImage, $progressEvent, $progressImage, $isProgressImageResolving, $isTemporarilyShowingSelectedImage } =
34+
useImageViewerContext();
3435
const progressEvent = useStore($progressEvent);
3536
const progressImage = useStore($progressImage);
37+
const isProgressImageResolving = useStore($isProgressImageResolving);
38+
const isTemporarilyShowingSelectedImage = useStore($isTemporarilyShowingSelectedImage);
3639
const [imageToRender, setImageToRender] = useState<ImageDTO | null>(null);
40+
const previousRenderedImageNameRef = useRef<string | null>(null);
41+
const selectedImageRevealTimeoutId = useRef(0);
3742

3843
useEffect(() => {
3944
if (!selectedImageName) {
@@ -76,6 +81,53 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
7681
};
7782
}, [imageDTO, imageToRender?.image_name, selectedImageName]);
7883

84+
const hasProgressImage = progressImage !== null;
85+
86+
useEffect(() => {
87+
const renderedImageName = imageToRender?.image_name ?? null;
88+
const previousRenderedImageName = previousRenderedImageNameRef.current;
89+
previousRenderedImageNameRef.current = renderedImageName;
90+
91+
window.clearTimeout(selectedImageRevealTimeoutId.current);
92+
93+
if (
94+
!shouldShowProgressInViewer ||
95+
!hasProgressImage ||
96+
isProgressImageResolving ||
97+
!renderedImageName ||
98+
renderedImageName !== selectedImageName
99+
) {
100+
$isTemporarilyShowingSelectedImage.set(false);
101+
return;
102+
}
103+
104+
if (previousRenderedImageName === null || previousRenderedImageName === renderedImageName) {
105+
return;
106+
}
107+
108+
$isTemporarilyShowingSelectedImage.set(true);
109+
selectedImageRevealTimeoutId.current = window.setTimeout(() => {
110+
$isTemporarilyShowingSelectedImage.set(false);
111+
}, SELECTED_IMAGE_REVEAL_DURATION_MS);
112+
113+
return () => {
114+
window.clearTimeout(selectedImageRevealTimeoutId.current);
115+
};
116+
}, [
117+
$isTemporarilyShowingSelectedImage,
118+
hasProgressImage,
119+
imageToRender?.image_name,
120+
isProgressImageResolving,
121+
selectedImageName,
122+
shouldShowProgressInViewer,
123+
]);
124+
125+
useEffect(() => {
126+
return () => {
127+
$isTemporarilyShowingSelectedImage.set(false);
128+
};
129+
}, [$isTemporarilyShowingSelectedImage]);
130+
79131
// Show and hide the next/prev buttons on mouse move
80132
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
81133
const timeoutId = useRef(0);
@@ -133,7 +185,7 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
133185
dependencies: [onHotkeyNextImage],
134186
});
135187

136-
const withProgress = shouldShowProgressInViewer && progressImage !== null;
188+
const withProgress = shouldShowProgressInViewer && hasProgressImage && !isTemporarilyShowingSelectedImage;
137189

138190
return (
139191
<Flex
@@ -202,3 +254,5 @@ const exit: AnimationProps['exit'] = {
202254
opacity: 0,
203255
transition: { duration: 0.07 },
204256
};
257+
258+
const SELECTED_IMAGE_REVEAL_DURATION_MS = 2000;

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export const ToggleMetadataViewerButton = memo(() => {
1515
const dispatch = useAppDispatch();
1616
const ctx = useImageViewerContext();
1717
const hasProgressImage = useStore(ctx.$hasProgressImage);
18+
const isTemporarilyShowingSelectedImage = useStore(ctx.$isTemporarilyShowingSelectedImage);
1819
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
1920

20-
const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer;
21+
const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer && !isTemporarilyShowingSelectedImage;
2122

2223
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
2324
const imageDTO = useAppSelector(selectLastSelectedItem);

invokeai/frontend/web/src/features/gallery/components/ImageViewer/context.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { useAppSelector } from 'app/store/storeHooks';
44
import { selectAutoSwitch } from 'features/gallery/store/gallerySelectors';
55
import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common';
66
import { LRUCache } from 'lru-cache';
7-
import { type Atom, atom, computed } from 'nanostores';
7+
import { type Atom, atom, computed, type WritableAtom } from 'nanostores';
88
import type { PropsWithChildren } from 'react';
9-
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
9+
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
1010
import type { S } from 'services/api/types';
1111
import { $socket } from 'services/events/stores';
1212
import { assert } from 'tsafe';
@@ -16,6 +16,8 @@ type ImageViewerContextValue = {
1616
$progressEvent: Atom<S['InvocationProgressEvent'] | null>;
1717
$progressImage: Atom<ProgressImageType | null>;
1818
$hasProgressImage: Atom<boolean>;
19+
$isProgressImageResolving: Atom<boolean>;
20+
$isTemporarilyShowingSelectedImage: WritableAtom<boolean>;
1921
onLoadImage: () => void;
2022
};
2123

@@ -29,6 +31,9 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
2931
const $progressEvent = useState(() => atom<S['InvocationProgressEvent'] | null>(null))[0];
3032
const $progressImage = useState(() => atom<ProgressImageType | null>(null))[0];
3133
const $hasProgressImage = useState(() => computed($progressImage, (progressImage) => progressImage !== null))[0];
34+
const $isProgressImageResolving = useState(() => atom(false))[0];
35+
const $isTemporarilyShowingSelectedImage = useState(() => atom(false))[0];
36+
const shouldClearProgressImageOnLoadRef = useRef(false);
3237
// We can have race conditions where we receive a progress event for a queue item that has already finished. Easiest
3338
// way to handle this is to keep track of finished queue items in a cache and ignore progress events for those.
3439
const [finishedQueueItemIds] = useState(() => new LRUCache<number, boolean>({ max: 200 }));
@@ -46,6 +51,8 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
4651
);
4752
return;
4853
}
54+
shouldClearProgressImageOnLoadRef.current = false;
55+
$isProgressImageResolving.set(false);
4956
$progressEvent.set(data);
5057
if (data.image) {
5158
$progressImage.set(data.image);
@@ -57,7 +64,7 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
5764
return () => {
5865
socket.off('invocation_progress', onInvocationProgress);
5966
};
60-
}, [$progressEvent, $progressImage, finishedQueueItemIds, socket]);
67+
}, [$isProgressImageResolving, $progressEvent, $progressImage, finishedQueueItemIds, socket]);
6168

6269
useEffect(() => {
6370
if (!socket) {
@@ -92,8 +99,13 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
9299
// will be stuck on the viewer.
93100
(data.origin === 'canvas' && data.destination !== 'canvas')
94101
) {
102+
shouldClearProgressImageOnLoadRef.current = false;
103+
$isProgressImageResolving.set(false);
95104
$progressEvent.set(null);
96105
$progressImage.set(null);
106+
} else {
107+
shouldClearProgressImageOnLoadRef.current = true;
108+
$isProgressImageResolving.set(true);
97109
}
98110
}
99111
};
@@ -103,16 +115,36 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
103115
return () => {
104116
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
105117
};
106-
}, [$progressEvent, $progressImage, autoSwitch, finishedQueueItemIds, socket]);
118+
}, [$isProgressImageResolving, $progressEvent, $progressImage, autoSwitch, finishedQueueItemIds, socket]);
107119

108120
const onLoadImage = useCallback(() => {
121+
if (!shouldClearProgressImageOnLoadRef.current) {
122+
return;
123+
}
124+
125+
shouldClearProgressImageOnLoadRef.current = false;
126+
$isProgressImageResolving.set(false);
109127
$progressEvent.set(null);
110128
$progressImage.set(null);
111-
}, [$progressEvent, $progressImage]);
129+
}, [$isProgressImageResolving, $progressEvent, $progressImage]);
112130

113131
const value = useMemo(
114-
() => ({ $progressEvent, $progressImage, $hasProgressImage, onLoadImage }),
115-
[$hasProgressImage, $progressEvent, $progressImage, onLoadImage]
132+
() => ({
133+
$progressEvent,
134+
$progressImage,
135+
$hasProgressImage,
136+
$isProgressImageResolving,
137+
$isTemporarilyShowingSelectedImage,
138+
onLoadImage,
139+
}),
140+
[
141+
$hasProgressImage,
142+
$isProgressImageResolving,
143+
$isTemporarilyShowingSelectedImage,
144+
$progressEvent,
145+
$progressImage,
146+
onLoadImage,
147+
]
116148
);
117149

118150
return <ImageViewerContext.Provider value={value}>{props.children}</ImageViewerContext.Provider>;

0 commit comments

Comments
 (0)