locate in gallery image context menu

This commit is contained in:
Attila Cseh
2025-08-17 16:39:10 +02:00
committed by psychedelicious
parent 9c1eb263a8
commit 19cd6eed08
7 changed files with 136 additions and 23 deletions

View File

@@ -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:",

View File

@@ -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(() => {
<Button
size="sm"
variant="ghost"
onClick={collapsibleApi.toggle}
onClick={galleryPanel.toggle}
leftIcon={isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
noOfLines={1}
>

View File

@@ -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 (
<MenuItem icon={<PiCrosshair />} onClickCapture={onClick} isDisabled={!isGalleryImage}>
{t('boards.locateInGalery')}
</MenuItem>
);
});
ImageMenuItemLocateInGalery.displayName = 'ImageMenuItemLocateInGalery';

View File

@@ -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) =
<MenuDivider />
<ImageMenuItemChangeBoard />
<ImageMenuItemStarUnstar />
<ImageMenuItemLocateInGalery />
</ImageDTOContextProvider>
);
};

View File

@@ -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.
*

View File

@@ -28,11 +28,15 @@ export const useCollapsibleGridviewPanel = (
const lastExpandedSizeRef = useRef<number>(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 {

View File

@@ -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
);
};