diff --git a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx index c18bc04379..8d94b4b39a 100644 --- a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx @@ -1,13 +1,14 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/focus'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { useImageActions } from 'features/gallery/hooks/useImageActions'; +import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow'; +import { useRecallAll } from 'features/gallery/hooks/useRecallAll'; +import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions'; +import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts'; +import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix'; +import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useImageDTO } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; @@ -26,64 +27,61 @@ export const GlobalImageHotkeys = memo(() => { GlobalImageHotkeys.displayName = 'GlobalImageHotkeys'; const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => { - const isGalleryFocused = useIsRegionFocused('gallery'); - const isViewerFocused = useIsRegionFocused('viewer'); - const imageActions = useImageActions(imageDTO); - const isStaging = useAppSelector(selectIsStaging); - const activeTab = useAppSelector(selectActiveTab); - const isUpscalingEnabled = useFeatureStatus('upscaling'); - - const isCanvasTabAndStaging = useMemo(() => activeTab === 'canvas' && isStaging, [activeTab, isStaging]); + const recallAll = useRecallAll(imageDTO); + const recallRemix = useRecallRemix(imageDTO); + const recallPrompts = useRecallPrompts(imageDTO); + const recallSeed = useRecallSeed(imageDTO); + const recallDimensions = useRecallDimensions(imageDTO); + const loadWorkflow = useLoadWorkflow(imageDTO); useRegisteredHotkeys({ id: 'loadWorkflow', category: 'viewer', - callback: imageActions.loadWorkflow, - options: { enabled: isGalleryFocused || isViewerFocused }, - dependencies: [imageActions.loadWorkflow, isGalleryFocused, isViewerFocused], + callback: loadWorkflow.load, + options: { enabled: loadWorkflow.isEnabled }, + dependencies: [loadWorkflow], }); + useRegisteredHotkeys({ id: 'recallAll', category: 'viewer', - callback: imageActions.recallAll, - options: { enabled: !isCanvasTabAndStaging && (isGalleryFocused || isViewerFocused) }, - dependencies: [imageActions.recallAll, isCanvasTabAndStaging, isGalleryFocused, isViewerFocused], + callback: recallAll.recall, + options: { enabled: recallAll.isEnabled }, + dependencies: [recallAll], }); + useRegisteredHotkeys({ id: 'recallSeed', category: 'viewer', - callback: imageActions.recallSeed, - options: { enabled: isGalleryFocused || isViewerFocused }, - dependencies: [imageActions.recallSeed, isGalleryFocused, isViewerFocused], + callback: recallSeed.recall, + options: { enabled: recallSeed.isEnabled }, + dependencies: [recallSeed], }); + useRegisteredHotkeys({ id: 'recallPrompts', category: 'viewer', - callback: imageActions.recallPrompts, - options: { enabled: isGalleryFocused || isViewerFocused }, - dependencies: [imageActions.recallPrompts, isGalleryFocused, isViewerFocused], + callback: recallPrompts.recall, + options: { enabled: recallPrompts.isEnabled }, + dependencies: [recallPrompts], }); + useRegisteredHotkeys({ id: 'remix', category: 'viewer', - callback: imageActions.remix, - options: { enabled: isGalleryFocused || isViewerFocused }, - dependencies: [imageActions.remix, isGalleryFocused, isViewerFocused], + callback: recallRemix.recall, + options: { enabled: recallRemix.isEnabled }, + dependencies: [recallRemix], }); + useRegisteredHotkeys({ id: 'useSize', category: 'viewer', - callback: imageActions.recallSize, - options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) }, - dependencies: [imageActions.recallSize, isStaging, isGalleryFocused, isViewerFocused], - }); - useRegisteredHotkeys({ - id: 'runPostprocessing', - category: 'viewer', - callback: imageActions.upscale, - options: { enabled: isUpscalingEnabled && isViewerFocused }, - dependencies: [isUpscalingEnabled, imageDTO, isViewerFocused], + callback: recallDimensions.recall, + options: { enabled: recallDimensions.isEnabled }, + dependencies: [recallDimensions], }); + return null; }); diff --git a/invokeai/frontend/web/src/common/hooks/useAsyncState.ts b/invokeai/frontend/web/src/common/hooks/useAsyncState.ts new file mode 100644 index 0000000000..61291aa1ec --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useAsyncState.ts @@ -0,0 +1,115 @@ +import { useStore } from '@nanostores/react'; +import { WrappedError } from 'common/util/result'; +import type { Atom } from 'nanostores'; +import { atom } from 'nanostores'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type SuccessState = { + status: 'success'; + value: T; + error: null; +}; + +type ErrorState = { + status: 'error'; + value: null; + error: Error; +}; + +type PendingState = { + status: 'pending'; + value: null; + error: null; +}; + +type IdleState = { + status: 'idle'; + value: null; + error: null; +}; + +export type State = IdleState | PendingState | SuccessState | ErrorState; + +type UseAsyncStateOptions = { + immediate?: boolean; +}; + +type UseAsyncReturn = { + $state: Atom>; + trigger: () => Promise; + reset: () => void; +}; + +export const useAsyncState = (execute: () => Promise, options?: UseAsyncStateOptions): UseAsyncReturn => { + const $state = useState(() => + atom>({ + status: 'idle', + value: null, + error: null, + }) + )[0]; + + const trigger = useCallback(async () => { + $state.set({ + status: 'pending', + value: null, + error: null, + }); + try { + const value = await execute(); + $state.set({ + status: 'success', + value, + error: null, + }); + } catch (error) { + $state.set({ + status: 'error', + value: null, + error: WrappedError.wrap(error), + }); + } + }, [$state, execute]); + + const reset = useCallback(() => { + $state.set({ + status: 'idle', + value: null, + error: null, + }); + }, [$state]); + + useEffect(() => { + if (options?.immediate) { + trigger(); + } + }, [options?.immediate, trigger]); + + const api = useMemo( + () => + ({ + $state, + trigger, + reset, + }) satisfies UseAsyncReturn, + [$state, trigger, reset] + ); + + return api; +}; + +type UseAsyncReturnReactive = { + state: State; + trigger: () => Promise; + reset: () => void; +}; + +export const useAsyncStateReactive = ( + execute: () => Promise, + options?: UseAsyncStateOptions +): UseAsyncReturnReactive => { + const { $state, trigger, reset } = useAsyncState(execute, options); + const state = useStore($state); + + return { state, trigger, reset }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx index 6a99146f62..b5fb2cd813 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx @@ -1,10 +1,11 @@ import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppStore } from 'app/store/storeHooks'; import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared'; import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice'; -import { canvasQueueItemDiscarded, selectDiscardedItems } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + buildSelectSessionQueueItems, + canvasQueueItemDiscarded, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { ProgressImage } from 'features/nodes/types/common'; import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores'; import { atom, computed, effect, map, subscribeKeys } from 'nanostores'; @@ -217,25 +218,9 @@ export const CanvasSessionContextProvider = memo( )[0]; /** - * A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable - * references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled - * items) should be done in a nanostores computed. + * A redux selector to select all queue items from the RTK Query cache. */ - const selectQueueItems = useMemo( - () => - createSelector( - [queueApi.endpoints.listAllQueueItems.select({ destination: session.id }), selectDiscardedItems], - ({ data }, discardedItems) => { - if (!data) { - return EMPTY_ARRAY; - } - return data.filter( - ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id) - ); - } - ), - [session.id] - ); + const selectQueueItems = useMemo(() => buildSelectSessionQueueItems(session.id), [session.id]); const discard = useCallback( (itemId: number) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 3ac40b5a8a..8f822c71d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,7 +1,9 @@ import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import { EMPTY_ARRAY } from 'app/store/constants'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { canvasReset } from 'features/controlLayers/store/actions'; +import { queueApi } from 'services/api/endpoints/queue'; type CanvasStagingAreaState = { generateSessionId: string | null; @@ -78,7 +80,30 @@ export const selectGenerateSessionId = createSelector( selectCanvasSessionSlice, ({ generateSessionId }) => generateSessionId ); -export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null); +export const buildSelectSessionQueueItems = (sessionId: string) => + createSelector( + [queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems], + ({ data }, discardedItems) => { + if (!data) { + return EMPTY_ARRAY; + } + return data.filter( + ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id) + ); + } + ); + +export const selectIsStaging = (state: RootState) => { + const sessionId = selectCanvasSessionId(state); + const { data } = queueApi.endpoints.listAllQueueItems.select({ destination: sessionId })(state); + if (!data) { + return false; + } + const discardedItems = selectDiscardedItems(state); + return data.some( + ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id) + ); +}; export const selectDiscardedItems = createSelector( selectCanvasSessionSlice, ({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx index 3df7be7e04..07f774edf0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx @@ -1,7 +1,11 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext'; -import { useImageActions } from 'features/gallery/hooks/useImageActions'; +import { useCreateStylePresetFromMetadata } from 'features/gallery/hooks/useCreateStylePresetFromMetadata'; +import { useRecallAll } from 'features/gallery/hooks/useRecallAll'; +import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts'; +import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix'; +import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -15,11 +19,15 @@ import { export const ImageMenuItemMetadataRecallActions = memo(() => { const { t } = useTranslation(); - const imageDTO = useImageDTOContext(); const subMenu = useSubMenu(); - const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } = - useImageActions(imageDTO); + const imageDTO = useImageDTOContext(); + + const recallAll = useRecallAll(imageDTO); + const recallRemix = useRecallRemix(imageDTO); + const recallPrompts = useRecallPrompts(imageDTO); + const recallSeed = useRecallSeed(imageDTO); + const stylePreset = useCreateStylePresetFromMetadata(imageDTO); return ( }> @@ -28,19 +36,23 @@ export const ImageMenuItemMetadataRecallActions = memo(() => { - } onClick={remix} isDisabled={!hasMetadata}> + } + onClick={recallRemix.recall} + isDisabled={!recallRemix.isEnabled} + > {t('parameters.remixImage')} - } onClick={recallPrompts} isDisabled={!hasPrompts}> + } onClick={recallPrompts.recall} isDisabled={!recallPrompts.isEnabled}> {t('parameters.usePrompt')} - } onClick={recallSeed} isDisabled={!hasSeed}> + } onClick={recallSeed.recall} isDisabled={!recallSeed.isEnabled}> {t('parameters.useSeed')} - } onClick={recallAll} isDisabled={!hasMetadata}> + } onClick={recallAll.recall} isDisabled={!recallAll.isEnabled}> {t('parameters.useAll')} - } onClick={createAsPreset} isDisabled={!hasPrompts}> + } onClick={stylePreset.create} isDisabled={!stylePreset.isEnabled}> {t('stylePresets.useForTemplate')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx index 4137d2e9ba..23ee544173 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAppStore } from 'app/store/storeHooks'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { NewLayerIcon } from 'features/controlLayers/components/common/icons'; import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy'; @@ -9,7 +9,6 @@ import { createNewCanvasEntityFromImage } from 'features/imageActions/actions'; import { toast } from 'features/toast/toast'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -20,7 +19,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => { const store = useAppStore(); const imageDTO = useImageDTOContext(); const isBusy = useCanvasIsBusySafe(); - const activeTab = useAppSelector(selectActiveTab); const onClickNewRasterLayerFromImage = useCallback(async () => { const { dispatch, getState } = store; @@ -82,10 +80,6 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => { }); }, [imageDTO, store, t]); - if (activeTab === 'generate') { - return null; - } - return ( }> 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 d664a5dc74..c2800083be 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,4 +1,5 @@ import { MenuDivider } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { IconMenuItemGroup } from 'common/components/IconMenuItem'; import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard'; import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy'; @@ -16,6 +17,7 @@ import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContex import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage'; import { ImageMenuItemUseForPromptGeneration } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseForPromptGeneration'; import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import type { ImageDTO } from 'services/api/types'; @@ -24,6 +26,8 @@ type SingleSelectionMenuItemsProps = { }; const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => { + const tab = useAppSelector(selectActiveTab); + return ( @@ -36,13 +40,13 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) = - + {(tab === 'canvas' || tab === 'generate') && } - + {(tab === 'canvas' || tab === 'generate') && } - + {tab === 'canvas' && } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 5c76b05d56..4af163bc22 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,21 +1,21 @@ import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useAppSelector } from 'app/store/storeHooks'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; -import { useImageActions } from 'features/gallery/hooks/useImageActions'; +import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage'; +import { useEditImage } from 'features/gallery/hooks/useEditImage'; +import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow'; +import { useRecallAll } from 'features/gallery/hooks/useRecallAll'; +import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions'; +import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts'; +import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix'; +import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; -import { newCanvasFromImage } from 'features/imageActions/actions'; -import { $hasTemplates } from 'features/nodes/store/nodesSlice'; import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { toast } from 'features/toast/toast'; -import { navigationApi } from 'features/ui/layouts/navigation-api'; -import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; -import { selectActiveTab, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useMemo } from 'react'; +import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, @@ -40,41 +40,19 @@ export const CurrentImageButtons = memo(() => { const imageName = useAppSelector(selectLastSelectedImage); const imageDTO = useImageDTO(imageName); - const hasTemplates = useStore($hasTemplates); - const imageActions = useImageActions(imageDTO); - const isStaging = useAppSelector(selectIsStaging); - const activeTab = useAppSelector(selectActiveTab); + const isUpscalingEnabled = useFeatureStatus('upscaling'); - const { getState, dispatch } = useAppStore(); - const canvasManager = useCanvasManagerSafe(); - const isCanvasTabAndStaging = useMemo(() => activeTab === 'canvas' && isStaging, [activeTab, isStaging]); + const recallAll = useRecallAll(imageDTO); + const recallRemix = useRecallRemix(imageDTO); + const recallPrompts = useRecallPrompts(imageDTO); + const recallSeed = useRecallSeed(imageDTO); + const recallDimensions = useRecallDimensions(imageDTO); + const loadWorkflow = useLoadWorkflow(imageDTO); + const editImage = useEditImage(imageDTO); + const deleteImage = useDeleteImage(imageDTO); - const handleEdit = useCallback(async () => { - if (!imageDTO) { - return; - } - - await newCanvasFromImage({ - imageDTO, - type: 'raster_layer', - withInpaintMask: true, - getState, - dispatch, - }); - navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID); - - // Automatically select the brush tool when editing an image - if (canvasManager) { - canvasManager.tool.$tool.set('brush'); - } - - toast({ - id: 'SENT_TO_CANVAS', - title: t('toast.sentToCanvas'), - status: 'success', - }); - }, [imageDTO, getState, dispatch, t, canvasManager]); + console.log(isDisabledOverride, recallSeed.isEnabled); return ( <> @@ -95,8 +73,8 @@ export const CurrentImageButtons = memo(() => {