Skip to content

Commit df7f302

Browse files
committed
Reset zoom state when changing media
1 parent 9200adb commit df7f302

3 files changed

Lines changed: 166 additions & 59 deletions

File tree

frontend/src/components/Media/MediaView.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,25 @@ export function MediaView({
5151

5252
// Custom hooks
5353
const { viewState, handlers } = useImageViewControls();
54+
const resetViewerState = useCallback(() => {
55+
handlers.resetZoom();
56+
setResetSignal((s) => s + 1);
57+
}, [handlers]);
58+
5459
// Navigation handlers
5560
const handleNextImage = useCallback(() => {
5661
if (currentViewIndex < images.length - 1) {
5762
dispatch(setCurrentViewIndex(currentViewIndex + 1));
58-
handlers.resetZoom();
63+
resetViewerState();
5964
}
60-
}, [dispatch, handlers, currentViewIndex, images.length]);
65+
}, [dispatch, resetViewerState, currentViewIndex, images.length]);
6166

6267
const handlePreviousImage = useCallback(() => {
6368
if (currentViewIndex > 0) {
6469
dispatch(setCurrentViewIndex(currentViewIndex - 1));
65-
handlers.resetZoom();
70+
resetViewerState();
6671
}
67-
}, [dispatch, handlers, currentViewIndex]);
72+
}, [dispatch, resetViewerState, currentViewIndex]);
6873

6974
const handleClose = useCallback(() => {
7075
dispatch(closeImageView());
@@ -74,9 +79,9 @@ export function MediaView({
7479
const handleThumbnailClick = useCallback(
7580
(index: number) => {
7681
dispatch(setCurrentViewIndex(index));
77-
handlers.resetZoom();
82+
resetViewerState();
7883
},
79-
[dispatch, handlers],
84+
[dispatch, resetViewerState],
8085
);
8186

8287
const location = useLocation();
@@ -85,8 +90,8 @@ export function MediaView({
8590
// Loop to first image handler for slideshow
8691
const handleLoopToStart = useCallback(() => {
8792
dispatch(setCurrentViewIndex(0));
88-
handlers.resetZoom();
89-
}, [dispatch, handlers]);
93+
resetViewerState();
94+
}, [dispatch, resetViewerState]);
9095

9196
// Slideshow functionality
9297
const { isSlideshowActive, toggleSlideshow } = useSlideshow(
@@ -142,9 +147,8 @@ export function MediaView({
142147

143148
const handleResetZoom = useCallback(() => {
144149
imageViewerRef.current?.reset();
145-
handlers.resetZoom();
146-
setResetSignal((s) => s + 1);
147-
}, [handlers]);
150+
resetViewerState();
151+
}, [resetViewerState]);
148152

149153
// Keyboard navigation
150154
useKeyboardNavigation({
@@ -164,6 +168,7 @@ export function MediaView({
164168

165169
// Safe variables
166170
const currentImagePath = currentImage.path;
171+
const currentImageKey = currentImage.id || currentImage.path;
167172
// console.log(currentImage);
168173
const currentImageAlt = `image-${currentViewIndex}`;
169174
return (
@@ -190,6 +195,7 @@ export function MediaView({
190195
>
191196
{type === 'image' && (
192197
<ImageViewer
198+
key={currentImageKey}
193199
ref={imageViewerRef}
194200
imagePath={currentImagePath}
195201
alt={currentImageAlt}

frontend/src/components/Media/ZoomableImage.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
159159
const imageRef = useRef<HTMLImageElement>(null);
160160
const isFitInitializedRef = useRef(false);
161161
const hasUserInteractedRef = useRef(false);
162+
const hasPendingFitRef = useRef(true);
162163
const fitTransformRef = useRef<FitTransform | null>(null);
163164
const fitFrameRef = useRef<number | null>(null);
164165
const [isOverflowing, setIsOverflowing] = useState(false);
@@ -246,6 +247,13 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
246247
fitFrameRef.current = null;
247248
}, []);
248249

250+
const markFitPending = useCallback(() => {
251+
isFitInitializedRef.current = false;
252+
hasUserInteractedRef.current = false;
253+
hasPendingFitRef.current = true;
254+
fitTransformRef.current = null;
255+
}, []);
256+
249257
const applyFitTransform = useCallback(
250258
(duration = 200, animationType: AnimationType = 'easeOut') => {
251259
const viewportElement = getViewportElement();
@@ -268,6 +276,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
268276
fitTransformRef.current = fitTransform;
269277
isFitInitializedRef.current = true;
270278
hasUserInteractedRef.current = false;
279+
hasPendingFitRef.current = false;
271280
setMinimumScale(fitTransform.scale);
272281
transformRef.current.setTransform(
273282
fitTransform.positionX,
@@ -301,11 +310,10 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
301310

302311
const handleReset = useCallback(
303312
(duration = 200, animationType: AnimationType = 'easeOut') => {
304-
isFitInitializedRef.current = false;
305-
hasUserInteractedRef.current = false;
313+
markFitPending();
306314
scheduleFitTransform(duration, animationType);
307315
},
308-
[scheduleFitTransform],
316+
[markFitPending, scheduleFitTransform],
309317
);
310318

311319
useImperativeHandle(ref, () => ({
@@ -326,10 +334,9 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
326334

327335
useEffect(() => {
328336
rotationRef.current = rotation;
329-
isFitInitializedRef.current = false;
330-
hasUserInteractedRef.current = false;
337+
markFitPending();
331338
scheduleFitTransform(0);
332-
}, [rotation, scheduleFitTransform]);
339+
}, [rotation, markFitPending, scheduleFitTransform]);
333340

334341
useEffect(() => {
335342
const viewportElement = getViewportElement();
@@ -358,24 +365,20 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
358365

359366
useEffect(() => {
360367
setIsOverflowing(false);
361-
isFitInitializedRef.current = false;
362-
hasUserInteractedRef.current = false;
363-
fitTransformRef.current = null;
368+
markFitPending();
364369

365370
const img = imageRef.current;
366371
if (!img) return;
367372

368-
const handleImageLoad = () => {
369-
scheduleFitTransform(0);
370-
};
371-
372373
if (img.complete && img.naturalWidth > 0) {
373-
handleImageLoad();
374-
} else {
375-
img.addEventListener('load', handleImageLoad);
376-
return () => img.removeEventListener('load', handleImageLoad);
374+
scheduleFitTransform(0);
377375
}
378-
}, [imagePath, scheduleFitTransform]);
376+
}, [imagePath, markFitPending, scheduleFitTransform]);
377+
378+
const handleImageLoad = useCallback(() => {
379+
markFitPending();
380+
scheduleFitTransform(0);
381+
}, [markFitPending, scheduleFitTransform]);
379382

380383
useEffect(() => {
381384
const wheelElement = wheelAreaRef.current;
@@ -458,11 +461,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
458461
fitTransformRef.current = fitTransform;
459462

460463
const shouldUseFitTransform =
461-
!isFitInitializedRef.current &&
462-
fitTransform.scale < MIN_SCALE - SCALE_EPSILON &&
463-
transformState.scale > fitTransform.scale + SCALE_EPSILON &&
464-
Math.abs(transformState.positionX) < SCALE_EPSILON &&
465-
Math.abs(transformState.positionY) < SCALE_EPSILON;
464+
hasPendingFitRef.current || !isFitInitializedRef.current;
466465
const currentScale = shouldUseFitTransform
467466
? fitTransform.scale
468467
: transformState.scale;
@@ -560,6 +559,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
560559

561560
setIsOverflowing(newOverflow.width || newOverflow.height);
562561
isFitInitializedRef.current = true;
562+
hasPendingFitRef.current = false;
563563
hasUserInteractedRef.current = true;
564564
transformRef.current.setTransform(targetX, targetY, newScale, 0);
565565
};
@@ -587,6 +587,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
587587
return (
588588
<div ref={wheelAreaRef} className="h-full w-full">
589589
<TransformWrapper
590+
key={imagePath}
590591
ref={transformRef}
591592
initialScale={minScale}
592593
minScale={minScale}
@@ -673,11 +674,13 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
673674
}}
674675
>
675676
<img
677+
key={imagePath}
676678
ref={imageRef}
677679
src={convertFileSrc(imagePath) || '/placeholder.svg'}
678680
alt={alt}
679681
draggable={false}
680682
className="select-none"
683+
onLoad={handleImageLoad}
681684
onError={(e) => {
682685
const img = e.target as HTMLImageElement;
683686
img.onerror = null;

0 commit comments

Comments
 (0)