Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@ import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import { AnimatePresence, motion, useMotionValue } from 'framer-motion';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useImageDTO } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';

import { useImageViewerContext } from './context';
import { NoContentForViewer } from './NoContentForViewer';
import { ProgressImage } from './ProgressImage2';
import { ProgressIndicator } from './ProgressIndicator2';
import { useSwipeNavigation } from './useSwipeNavigation';

export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | null }) => {
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
const { onLoadImage, $progressEvent, $progressImage } = useImageViewerContext();
const progressEvent = useStore($progressEvent);
const progressImage = useStore($progressImage);
const { onDragEnd, previousImageName, nextImageName } = useSwipeNavigation();

// Get adjacent images for swipe preview
const previousImageDTO = useImageDTO(previousImageName);
const nextImageDTO = useImageDTO(nextImageName);

// Track drag state for showing adjacent images
const dragX = useMotionValue(0);
const [isDragging, setIsDragging] = useState(false);

// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
Expand All @@ -38,28 +49,99 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu

const withProgress = shouldShowProgressInViewer && progressImage !== null;

// Reset drag state when image changes
useEffect(() => {
dragX.set(0);
setIsDragging(false);
}, [imageDTO?.image_name, dragX]);

const onDragStart = useCallback(() => {
setIsDragging(true);
}, []);

const handleDragEnd = useCallback(
(event: MouseEvent | TouchEvent | PointerEvent, info: { offset: { x: number; y: number } }) => {
setIsDragging(false);
onDragEnd(event, info);
},
[onDragEnd]
);

return (
<Flex
<Box
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
width="full"
height="full"
alignItems="center"
justifyContent="center"
position="relative"
overflow="hidden"
>
{imageDTO && (
<Flex
key={imageDTO.image_name}
w="full"
h="full"
position="absolute"
alignItems="center"
justifyContent="center"
>
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} borderRadius="base" />
</Flex>
)}
<Box
as={motion.div}
drag={imageDTO ? 'x' : false}
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.2}
onDragStart={onDragStart}
onDragEnd={handleDragEnd}
style={{ x: dragX }}
width="full"
height="full"
display="flex"
alignItems="center"
justifyContent="center"
position="relative"
>
{/* Previous image (shown when dragging right) */}
{isDragging && previousImageDTO && (
<Box
as={motion.div}
position="absolute"
right="100%"
top={0}
bottom={0}
width="full"
display="flex"
alignItems="center"
justifyContent="center"
pointerEvents="none"
>
<DndImage imageDTO={previousImageDTO} borderRadius="base" />
</Box>
)}

{/* Current image */}
{imageDTO && (
<Flex
key={imageDTO.image_name}
w="full"
h="full"
position="absolute"
alignItems="center"
justifyContent="center"
>
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} borderRadius="base" />
</Flex>
)}

{/* Next image (shown when dragging left) */}
{isDragging && nextImageDTO && (
<Box
as={motion.div}
position="absolute"
left="100%"
top={0}
bottom={0}
width="full"
display="flex"
alignItems="center"
justifyContent="center"
pointerEvents="none"
>
<DndImage imageDTO={nextImageDTO} borderRadius="base" />
</Box>
)}
</Box>

{!imageDTO && <NoContentForViewer />}
{withProgress && (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center" bg="base.900">
Expand Down Expand Up @@ -96,7 +178,7 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
</Box>
)}
</AnimatePresence>
</Flex>
</Box>
);
});
CurrentImagePreview.displayName = 'CurrentImagePreview';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clamp } from 'es-toolkit/compat';
import { useGalleryImageNames } from 'features/gallery/components/use-gallery-image-names';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { useCallback, useMemo } from 'react';

const SWIPE_THRESHOLD = 50; // Minimum distance in pixels to trigger swipe

export const useSwipeNavigation = () => {
const dispatch = useAppDispatch();
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const { imageNames, isFetching } = useGalleryImageNames();

const currentIndex = useMemo(
() => (lastSelectedItem ? imageNames.findIndex((n) => n === lastSelectedItem) : -1),
[imageNames, lastSelectedItem]
);

const isOnFirstItem = useMemo(() => currentIndex === 0, [currentIndex]);
const isOnLastItem = useMemo(() => currentIndex === imageNames.length - 1, [currentIndex, imageNames.length]);

const previousImageName = useMemo(
() => (currentIndex > 0 ? imageNames[currentIndex - 1] : null),
[currentIndex, imageNames]
);

const nextImageName = useMemo(
() => (currentIndex < imageNames.length - 1 ? imageNames[currentIndex + 1] : null),
[currentIndex, imageNames]
);

const navigateToPrevious = useCallback(() => {
if (isOnFirstItem || isFetching) {
return;
}
const targetIndex = currentIndex - 1;
const clampedIndex = clamp(targetIndex, 0, imageNames.length - 1);
const n = imageNames.at(clampedIndex);
if (!n) {
return;
}
dispatch(imageSelected(n));
}, [dispatch, imageNames, currentIndex, isOnFirstItem, isFetching]);

const navigateToNext = useCallback(() => {
if (isOnLastItem || isFetching) {
return;
}
const targetIndex = currentIndex + 1;
const clampedIndex = clamp(targetIndex, 0, imageNames.length - 1);
const n = imageNames.at(clampedIndex);
if (!n) {
return;
}
dispatch(imageSelected(n));
}, [dispatch, imageNames, currentIndex, isOnLastItem, isFetching]);

const onDragEnd = useCallback(
(_event: MouseEvent | TouchEvent | PointerEvent, info: { offset: { x: number; y: number } }) => {
// Swipe right (positive x) = go to previous image
// Swipe left (negative x) = go to next image
if (info.offset.x > SWIPE_THRESHOLD) {
navigateToPrevious();
} else if (info.offset.x < -SWIPE_THRESHOLD) {
navigateToNext();
}
},
[navigateToPrevious, navigateToNext]
);

return {
onDragEnd,
canNavigatePrevious: !isOnFirstItem,
canNavigateNext: !isOnLastItem,
previousImageName,
nextImageName,
};
};