diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index fae5029d36..28c31935ab 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -38,6 +38,7 @@
"deletedImagesCannotBeRestored": "Deleted images cannot be restored.",
"hideBoards": "Hide Boards",
"loading": "Loading...",
+ "locateInGalery": "Locate in Gallery",
"menuItemAutoAdd": "Auto-add to this Board",
"move": "Move",
"movingImagesToBoard_one": "Moving {{count}} image to board:",
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 6e9c323049..9c099cf5e2 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -7,13 +7,7 @@ import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useG
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
-import {
- GALLERY_PANEL_DEFAULT_HEIGHT_PX,
- GALLERY_PANEL_ID,
- GALLERY_PANEL_MIN_EXPANDED_HEIGHT_PX,
- GALLERY_PANEL_MIN_HEIGHT_PX,
-} from 'features/ui/layouts/shared';
-import { useCollapsibleGridviewPanel } from 'features/ui/layouts/use-collapsible-gridview-panel';
+import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel';
import type { CSSProperties } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -34,16 +28,8 @@ export const GalleryPanel = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { tab } = useAutoLayoutContext();
- const collapsibleApi = useCollapsibleGridviewPanel(
- tab,
- GALLERY_PANEL_ID,
- 'vertical',
- GALLERY_PANEL_DEFAULT_HEIGHT_PX,
- GALLERY_PANEL_MIN_HEIGHT_PX,
- GALLERY_PANEL_MIN_EXPANDED_HEIGHT_PX
- );
- const isCollapsed = useStore(collapsibleApi.$isCollapsed);
-
+ const galleryPanel = useGalleryPanel(tab);
+ const isCollapsed = useStore(galleryPanel.$isCollapsed);
const galleryView = useAppSelector(selectGalleryView);
const initialSearchTerm = useAppSelector(selectSearchTerm);
const searchDisclosure = useDisclosure(!!initialSearchTerm);
@@ -58,11 +44,11 @@ export const GalleryPanel = memo(() => {
const handleClickSearch = useCallback(() => {
onResetSearchTerm();
- if (!searchDisclosure.isOpen && collapsibleApi.$isCollapsed.get()) {
- collapsibleApi.expand();
+ if (!searchDisclosure.isOpen && galleryPanel.$isCollapsed.get()) {
+ galleryPanel.expand();
}
searchDisclosure.toggle();
- }, [collapsibleApi, onResetSearchTerm, searchDisclosure]);
+ }, [galleryPanel, onResetSearchTerm, searchDisclosure]);
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const boardName = useBoardName(selectedBoardId);
@@ -73,7 +59,7 @@ export const GalleryPanel = memo(() => {
: }
noOfLines={1}
>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLocateInGalery.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLocateInGalery.tsx
new file mode 100644
index 0000000000..9100f22f7b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLocateInGalery.tsx
@@ -0,0 +1,36 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
+import { boardIdSelected } from 'features/gallery/store/gallerySlice';
+import { navigationApi } from 'features/ui/layouts/navigation-api';
+import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCrosshair } from 'react-icons/pi';
+
+export const ImageMenuItemLocateInGalery = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const imageDTO = useImageDTOContext();
+ const activeTab = useAppSelector(selectActiveTab);
+ const galleryPanel = useGalleryPanel(activeTab);
+
+ const isGalleryImage = useMemo(() => {
+ return !!imageDTO.board_id;
+ }, [imageDTO]);
+
+ const onClick = useCallback(() => {
+ navigationApi.expandRightPanel();
+ galleryPanel.expand();
+ dispatch(boardIdSelected({ boardId: imageDTO.board_id ?? 'none', selectedImageName: imageDTO.image_name }));
+ }, [dispatch, galleryPanel, imageDTO]);
+
+ return (
+ } onClickCapture={onClick} isDisabled={!isGalleryImage}>
+ {t('boards.locateInGalery')}
+
+ );
+});
+
+ImageMenuItemLocateInGalery.displayName = 'ImageMenuItemLocateInGalery';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
index c50c3f72c5..2a304ff760 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
@@ -6,6 +6,7 @@ import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
+import { ImageMenuItemLocateInGalery } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLocateInGalery';
import { ImageMenuItemMetadataRecallActionsCanvasGenerateTabs } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs';
import { ImageMenuItemNewCanvasFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImageSubMenu';
import { ImageMenuItemNewLayerFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu';
@@ -55,6 +56,7 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
+
);
};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
index be4cf6ea1d..daa191acd7 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
@@ -448,6 +448,35 @@ export class NavigationApi {
return this.panels.get(key);
};
+ /**
+ * Expand the left panel in the currently active tab.
+ *
+ * This method will not wait for the panel to be registered.
+ *
+ * @returns True if the panel was expanded, false if it was not found or an error occurred
+ */
+ expandLeftPanel = (): boolean => {
+ const activeTab = this._app?.activeTab.get() ?? null;
+ if (!activeTab) {
+ log.warn('No active tab found to expand left panel');
+ return false;
+ }
+ const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID);
+ if (!leftPanel) {
+ log.warn(`Left panel not found in active tab "${activeTab}"`);
+ return false;
+ }
+
+ if (!(leftPanel instanceof GridviewPanel)) {
+ log.error(`Right panels must be instances of GridviewPanel`);
+ return false;
+ }
+
+ this._expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX);
+
+ return true;
+ };
+
/**
* Toggle the left panel in the currently active tab.
*
@@ -481,6 +510,35 @@ export class NavigationApi {
return true;
};
+ /**
+ * Expand the right panel in the currently active tab.
+ *
+ * This method will not wait for the panel to be registered.
+ *
+ * @returns True if the panel was expanded, false if it was not found or an error occurred
+ */
+ expandRightPanel = (): boolean => {
+ const activeTab = this._app?.activeTab.get() ?? null;
+ if (!activeTab) {
+ log.warn('No active tab found to expand right panel');
+ return false;
+ }
+ const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID);
+ if (!rightPanel) {
+ log.warn(`Right panel not found in active tab "${activeTab}"`);
+ return false;
+ }
+
+ if (!(rightPanel instanceof GridviewPanel)) {
+ log.error(`Right panels must be instances of GridviewPanel`);
+ return false;
+ }
+
+ this._expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX);
+
+ return true;
+ };
+
/**
* Toggle the right panel in the currently active tab.
*
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
index 1415d710ec..aa730a54a8 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
@@ -28,11 +28,15 @@ export const useCollapsibleGridviewPanel = (
const lastExpandedSizeRef = useRef(0);
const collapse = useCallback(() => {
const panel = navigationApi.getPanel(tab, panelId);
-
if (!panel || !(panel instanceof GridviewPanel)) {
return;
}
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ if (isCollapsed) {
+ return;
+ }
+
lastExpandedSizeRef.current = orientation === 'vertical' ? panel.height : panel.width;
if (orientation === 'vertical') {
@@ -48,6 +52,11 @@ export const useCollapsibleGridviewPanel = (
return;
}
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ if (!isCollapsed) {
+ return;
+ }
+
let newSize = lastExpandedSizeRef.current || defaultSize;
if (minExpandedSize && newSize < minExpandedSize) {
newSize = minExpandedSize;
@@ -58,7 +67,7 @@ export const useCollapsibleGridviewPanel = (
} else {
panel.api.setSize({ width: newSize });
}
- }, [defaultSize, minExpandedSize, orientation, panelId, tab]);
+ }, [defaultSize, minExpandedSize, orientation, collapsedSize, panelId, tab]);
const toggle = useCallback(() => {
const panel = navigationApi.getPanel(tab, panelId);
@@ -66,6 +75,7 @@ export const useCollapsibleGridviewPanel = (
return;
}
const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+
if (isCollapsed) {
expand();
} else {
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts
new file mode 100644
index 0000000000..274f4bd595
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts
@@ -0,0 +1,20 @@
+import type { TabName } from 'features/ui/store/uiTypes';
+
+import {
+ GALLERY_PANEL_DEFAULT_HEIGHT_PX,
+ GALLERY_PANEL_ID,
+ GALLERY_PANEL_MIN_EXPANDED_HEIGHT_PX,
+ GALLERY_PANEL_MIN_HEIGHT_PX,
+} from './shared';
+import { useCollapsibleGridviewPanel } from './use-collapsible-gridview-panel';
+
+export const useGalleryPanel = (tab: TabName) => {
+ return useCollapsibleGridviewPanel(
+ tab,
+ GALLERY_PANEL_ID,
+ 'vertical',
+ GALLERY_PANEL_DEFAULT_HEIGHT_PX,
+ GALLERY_PANEL_MIN_HEIGHT_PX,
+ GALLERY_PANEL_MIN_EXPANDED_HEIGHT_PX
+ );
+};