Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -13,6 +13,9 @@ import {
} from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { MutableRefObject } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import type {
Expand Down Expand Up @@ -80,22 +83,41 @@ const computeItemKey: GridComputeItemKey<string, GridContext> = (index, imageNam
return `${JSON.stringify(queryArgs)}-${imageName ?? index}`;
};

const canHandleGridArrowNavigation = (
activeTab: ReturnType<typeof selectActiveTab>,
focusedRegion: ReturnType<typeof getFocusedRegion>
) => {
if (navigationApi.isViewerArrowNavigationMode(activeTab)) {
// When gallery is not effectively available, viewer hotkeys own left/right navigation.
return false;
}

if (focusedRegion === 'gallery' || focusedRegion === 'viewer') {
return true;
}

// Fallback for tab-switch edge case: allow nav when viewer dock tab is active before first click.
return navigationApi.isDockviewPanelActive(activeTab, VIEWER_PANEL_ID);
};

/**
* Handles keyboard navigation for the gallery.
*/
const useKeyboardNavigation = (
imageNames: string[],
navigationImageNames: string[],
virtuosoRef: React.RefObject<VirtuosoGridHandle>,
rootRef: React.RefObject<HTMLDivElement>
) => {
const { dispatch, getState } = useAppStore();
const activeTab = useAppSelector(selectActiveTab);

const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (getFocusedRegion() !== 'gallery') {
// Only handle keyboard navigation when the gallery is focused
const focusedRegion = getFocusedRegion();
if (!canHandleGridArrowNavigation(activeTab, focusedRegion)) {
return;
}

// Only handle arrow keys
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
return;
Expand All @@ -112,7 +134,7 @@ const useKeyboardNavigation = (
return;
}

if (imageNames.length === 0) {
if (navigationImageNames.length === 0) {
return;
}

Expand All @@ -132,7 +154,7 @@ const useKeyboardNavigation = (
(selectImageToCompare(state) ?? selectLastSelectedItem(state))
: selectLastSelectedItem(state);

const currentIndex = getItemIndex(imageName ?? null, imageNames);
const currentIndex = getItemIndex(imageName ?? null, navigationImageNames);

let newIndex = currentIndex;

Expand All @@ -146,7 +168,7 @@ const useKeyboardNavigation = (
}
break;
case 'ArrowRight':
if (currentIndex < imageNames.length - 1) {
if (currentIndex < navigationImageNames.length - 1) {
newIndex = currentIndex + 1;
// } else {
// // Wrap to first image
Expand All @@ -163,16 +185,16 @@ const useKeyboardNavigation = (
break;
case 'ArrowDown':
// If no images below, stay on current image
if (currentIndex >= imageNames.length - imagesPerRow) {
if (currentIndex >= navigationImageNames.length - imagesPerRow) {
newIndex = currentIndex;
} else {
newIndex = Math.min(imageNames.length - 1, currentIndex + imagesPerRow);
newIndex = Math.min(navigationImageNames.length - 1, currentIndex + imagesPerRow);
}
break;
}

if (newIndex !== currentIndex && newIndex >= 0 && newIndex < imageNames.length) {
const newImageName = imageNames[newIndex];
if (newIndex !== currentIndex && newIndex >= 0 && newIndex < navigationImageNames.length) {
const newImageName = navigationImageNames[newIndex];
if (newImageName) {
if (event.altKey) {
dispatch(imageToCompareChanged(newImageName));
Expand All @@ -182,7 +204,7 @@ const useKeyboardNavigation = (
}
}
},
[rootRef, virtuosoRef, imageNames, getState, dispatch]
[activeTab, rootRef, virtuosoRef, navigationImageNames, getState, dispatch]
);

useRegisteredHotkeys({
Expand Down Expand Up @@ -316,13 +338,14 @@ const useStarImageHotkey = () => {

type GalleryImageGridContentProps = {
imageNames: string[];
navigationImageNames?: string[];
isLoading: boolean;
queryArgs: ListImageNamesQueryArgs;
rootRef?: React.RefObject<HTMLDivElement>;
};

export const GalleryImageGridContent = memo(
({ imageNames, isLoading, queryArgs, rootRef: rootRefProp }: GalleryImageGridContentProps) => {
({ imageNames, navigationImageNames, isLoading, queryArgs, rootRef: rootRefProp }: GalleryImageGridContentProps) => {
const virtuosoRef = useRef<VirtuosoGridHandle>(null);
const rangeRef = useRef<ListRange>({ startIndex: 0, endIndex: 0 });
const internalRootRef = useRef<HTMLDivElement>(null);
Expand All @@ -336,7 +359,7 @@ export const GalleryImageGridContent = memo(

useStarImageHotkey();
useKeepSelectedImageInView(imageNames, virtuosoRef, rootRef, rangeRef);
useKeyboardNavigation(imageNames, virtuosoRef, rootRef);
useKeyboardNavigation(navigationImageNames ?? imageNames, virtuosoRef, rootRef);
const scrollerRef = useScrollableGallery(rootRef);

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const GalleryImageGridPaged = memo(() => {
<Flex w="full" h="full">
<GalleryImageGridContent
imageNames={pageImageNames}
navigationImageNames={imageNames}
isLoading={false}
queryArgs={queryArgs}
rootRef={gridRootRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import { CanvasAlertsInvocationProgress } from 'features/controlLayers/component
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { useNextPrevItemNavigation } from 'features/gallery/components/useNextPrevItemNavigation';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import {
selectActiveTab,
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 { memo, useCallback, useEffect, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';

import { useImageViewerContext } from './context';
Expand All @@ -17,11 +25,56 @@ import { ProgressImage } from './ProgressImage2';
import { ProgressIndicator } from './ProgressIndicator2';

export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | null }) => {
const activeTab = useAppSelector(selectActiveTab);
const selectedImageName = useAppSelector(selectLastSelectedItem);
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
const { goToPreviousImage, goToNextImage, isFetching } = useNextPrevItemNavigation();
const { onLoadImage, $progressEvent, $progressImage } = useImageViewerContext();
const progressEvent = useStore($progressEvent);
const progressImage = useStore($progressImage);
const [imageToRender, setImageToRender] = useState<ImageDTO | null>(null);

useEffect(() => {
if (!selectedImageName) {
setImageToRender(null);
return;
}

if (!imageDTO || imageToRender?.image_name === imageDTO.image_name) {
return;
}

let canceled = false;

const onReady = () => {
if (canceled) {
return;
}
setImageToRender(imageDTO);
};

if (typeof window === 'undefined') {
onReady();
return;
}

const preloader = new window.Image();

preloader.onload = onReady;
preloader.onerror = onReady;
preloader.src = imageDTO.image_url;

if (preloader.complete) {
onReady();
}

return () => {
canceled = true;
preloader.onload = null;
preloader.onerror = null;
};
}, [imageDTO, imageToRender?.image_name, selectedImageName]);

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

const handleViewerArrowNavigation = useCallback(
(event: KeyboardEvent, navigate: () => void) => {
if (!navigationApi.isViewerArrowNavigationMode(activeTab) || !imageToRender || isFetching) {
return;
}
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
return;
}
event.preventDefault();
navigate();
},
[activeTab, imageToRender, isFetching]
);

const onHotkeyPrevImage = useCallback(
(event: KeyboardEvent) => {
handleViewerArrowNavigation(event, goToPreviousImage);
},
[goToPreviousImage, handleViewerArrowNavigation]
);

const onHotkeyNextImage = useCallback(
(event: KeyboardEvent) => {
handleViewerArrowNavigation(event, goToNextImage);
},
[goToNextImage, handleViewerArrowNavigation]
);

useRegisteredHotkeys({
id: 'galleryNavLeft',
category: 'gallery',
callback: onHotkeyPrevImage,
options: { preventDefault: true },
dependencies: [onHotkeyPrevImage],
});

useRegisteredHotkeys({
id: 'galleryNavRight',
category: 'gallery',
callback: onHotkeyNextImage,
options: { preventDefault: true },
dependencies: [onHotkeyNextImage],
});

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

return (
Expand All @@ -48,19 +145,12 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
justifyContent="center"
position="relative"
>
{imageDTO && (
<Flex
key={imageDTO.image_name}
w="full"
h="full"
position="absolute"
alignItems="center"
justifyContent="center"
>
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} borderRadius="base" />
{imageToRender && (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<DndImage imageDTO={imageToRender} onLoad={onLoadImage} borderRadius="base" />
</Flex>
)}
{!imageDTO && <NoContentForViewer />}
{!imageToRender && <NoContentForViewer />}
{withProgress && (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center" bg="base.900">
<ProgressImage progressImage={progressImage} />
Expand All @@ -72,13 +162,13 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
<Flex flexDir="column" gap={2} position="absolute" top={0} insetInlineStart={0} alignItems="flex-start">
<CanvasAlertsInvocationProgress />
</Flex>
{shouldShowItemDetails && imageDTO && !withProgress && (
{shouldShowItemDetails && imageToRender && !withProgress && (
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
<ImageMetadataViewer image={imageDTO} />
<ImageMetadataViewer image={imageToRender} />
</Box>
)}
<AnimatePresence>
{shouldShowNextPrevButtons && imageDTO && (
{shouldShowNextPrevButtons && imageToRender && (
<Box
as={motion.div}
key="nextPrevButtons"
Expand Down
Loading