From 12ba15bfa91237ab29b4dd772570701a10e1e636 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Fri, 9 Aug 2024 16:00:13 -0400 Subject: [PATCH] UI updates per PR feedback --- invokeai/frontend/web/public/locales/en.json | 3 +- .../listeners/promptChanged.ts | 4 +- invokeai/frontend/web/src/app/store/store.ts | 7 +- .../common/hooks/useCopyImageToClipboard.ts | 7 +- .../web/src/common/hooks/useImageUrlToBlob.ts | 54 --------- .../src/common/util/convertImageUrlToBlob.ts | 33 ++++++ .../features/gallery/hooks/useImageActions.ts | 7 +- .../nodes/util/graph/graphBuilderUtils.ts | 39 ++++--- .../components/Core/ParamNegativePrompt.tsx | 39 +++---- .../components/Core/ParamPositivePrompt.tsx | 39 +++---- .../components/Prompts/ViewModePrompt.tsx | 82 ++++++-------- .../web/src/features/prompt/usePrompt.ts | 8 -- .../components/ActiveStylePreset.tsx | 103 ++++++++++-------- .../components/StylePresetForm.tsx | 20 ++-- .../components/StylePresetImageField.tsx | 4 +- .../components/StylePresetList.tsx | 2 +- .../components/StylePresetListItem.tsx | 33 +++--- .../components/StylePresetMenu.tsx | 10 +- .../components/StylePresetMenuTrigger.tsx | 16 +-- .../components/StylePresetModal.tsx | 48 +++++++- .../components/StylePresetPromptField.tsx | 20 ++-- .../hooks/usePresetModifiedPrompts.ts | 16 ++- .../store/stylePresetModalSlice.ts | 5 +- .../stylePresets/store/stylePresetSlice.ts | 25 ++++- .../src/features/stylePresets/store/types.ts | 14 ++- .../stylePresets/util/getViewModeChunks.tsx | 9 +- .../services/api/endpoints/stylePresets.ts | 2 +- 27 files changed, 340 insertions(+), 309 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts create mode 100644 invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 362023adc4..605c1260a4 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1705,7 +1705,7 @@ "name": "Name", "negativePrompt": "Negative Prompt", "noMatchingTemplates": "No matching templates", - "placeholderDirections": "Use the { } button to specify where your manual prompt should be included in the template. If you do not provide one, the template will be appended to your prompt.", + "placeholderDirections": "Use the button to specify where your manual prompt should be included in the template. If you do not provide one, the template will be appended to your prompt.", "positivePrompt": "Positive Prompt", "searchByName": "Search by name", "templateDeleted": "Prompt template deleted", @@ -1714,6 +1714,7 @@ "updatePromptTemplate": "Update Prompt Template", "uploadImage": "Upload Image", "useForTemplate": "Use For Prompt Template", + "viewList": "View Template List", "viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box." }, "upsell": { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index 098a92de41..513c087496 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -12,7 +12,7 @@ import { } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import { activeStylePresetChanged } from 'features/stylePresets/store/stylePresetSlice'; +import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { utilitiesApi } from 'services/api/endpoints/utilities'; import { socketConnected } from 'services/events/actions'; @@ -22,7 +22,7 @@ const matcher = isAnyOf( maxPromptsChanged, maxPromptsReset, socketConnected, - activeStylePresetChanged + activeStylePresetIdChanged ); export const addDynamicPromptsListener = (startAppListening: AppStartListening) => { diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index eba0ca4b15..e3a7f3702a 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -29,7 +29,7 @@ import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/up import { queueSlice } from 'features/queue/store/queueSlice'; import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { stylePresetModalSlice } from 'features/stylePresets/store/stylePresetModalSlice'; -import { stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice'; +import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice'; import { configSlice } from 'features/system/store/configSlice'; import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice'; @@ -118,6 +118,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [controlLayersPersistConfig.name]: controlLayersPersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, + [stylePresetPersistConfig.name]: stylePresetPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { @@ -168,8 +169,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) => reducer: rememberedRootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ - serializableCheck: false, - immutableCheck: false, + serializableCheck: import.meta.env.MODE === 'development', + immutableCheck: import.meta.env.MODE === 'development', }) .concat(api.middleware) .concat(dynamicMiddlewares) diff --git a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts index 233b841034..345ea98e13 100644 --- a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts +++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts @@ -1,4 +1,4 @@ -import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob'; +import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob'; import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'; export const useCopyImageToClipboard = () => { const { t } = useTranslation(); - const imageUrlToBlob = useImageUrlToBlob(); const isClipboardAPIAvailable = useMemo(() => { return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem); @@ -23,7 +22,7 @@ export const useCopyImageToClipboard = () => { }); } try { - const blob = await imageUrlToBlob(image_url); + const blob = await convertImageUrlToBlob(image_url); if (!blob) { throw new Error('Unable to create Blob'); @@ -45,7 +44,7 @@ export const useCopyImageToClipboard = () => { }); } }, - [imageUrlToBlob, isClipboardAPIAvailable, t] + [isClipboardAPIAvailable, t] ); return { isClipboardAPIAvailable, copyImageToClipboard }; diff --git a/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts b/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts deleted file mode 100644 index 4916dc974e..0000000000 --- a/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { $authToken } from 'app/store/nanostores/authToken'; -import { useCallback } from 'react'; - -/** - * Converts an image URL to a Blob by creating an element, drawing it to canvas - * and then converting the canvas to a Blob. - * - * @returns A function that takes a URL and returns a Promise that resolves with a Blob - */ -export const useImageUrlToBlob = () => { - const imageUrlToBlob = useCallback( - async (url: string, dimension?: number) => - new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - let width = img.width; - let height = img.height; - - if (dimension) { - const aspectRatio = img.width / img.height; - if (img.width > img.height) { - width = dimension; - height = dimension / aspectRatio; - } else { - height = dimension; - width = dimension * aspectRatio; - } - } - - canvas.width = width; - canvas.height = height; - - const context = canvas.getContext('2d'); - if (!context) { - return; - } - context.drawImage(img, 0, 0, width, height); - resolve( - new Promise((resolve) => { - canvas.toBlob(function (blob) { - resolve(blob); - }, 'image/png'); - }) - ); - }; - img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous'; - img.src = url; - }), - [] - ); - - return imageUrlToBlob; -}; diff --git a/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts new file mode 100644 index 0000000000..42fdd46609 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts @@ -0,0 +1,33 @@ +import { $authToken } from 'app/store/nanostores/authToken'; + +/** + * Converts an image URL to a Blob by creating an element, drawing it to canvas + * and then converting the canvas to a Blob. + * + * @returns A function that takes a URL and returns a Promise that resolves with a Blob + */ + +export const convertImageUrlToBlob = async (url: string) => + new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + const context = canvas.getContext('2d'); + if (!context) { + return; + } + context.drawImage(img, 0, 0); + resolve( + new Promise((resolve) => { + canvas.toBlob(function (blob) { + resolve(blob); + }, 'image/png'); + }) + ); + }; + img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous'; + img.src = url; + }); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index d29f417517..b6086f846f 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -1,6 +1,5 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob'; import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers'; import { isModalOpenChanged, prefilledFormDataChanged } from 'features/stylePresets/store/stylePresetModalSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; @@ -14,7 +13,6 @@ export const useImageActions = (image_name?: string) => { const [hasMetadata, setHasMetadata] = useState(false); const [hasSeed, setHasSeed] = useState(false); const [hasPrompts, setHasPrompts] = useState(false); - const imageUrlToBlob = useImageUrlToBlob(); const dispatch = useAppDispatch(); const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken); @@ -72,19 +70,18 @@ export const useImageActions = (image_name?: string) => { if (image_name && metadata && imageDTO) { const positivePrompt = await handlers.positivePrompt.parse(metadata); const negativePrompt = await handlers.negativePrompt.parse(metadata); - const imageBlob = await imageUrlToBlob(imageDTO.image_url, 100); dispatch( prefilledFormDataChanged({ name: '', positivePrompt, negativePrompt, - image: imageBlob ? new File([imageBlob], 'stylePreset.png', { type: 'image/png' }) : null, + imageUrl: imageDTO.image_url, }) ); dispatch(isModalOpenChanged(true)); } - }, [image_name, metadata, dispatch, imageDTO, imageUrlToBlob]); + }, [image_name, metadata, dispatch, imageDTO]); return { recallAll, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 00bd213c62..ed6dfbc224 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -2,6 +2,7 @@ import type { RootState } from 'app/store/store'; import type { BoardField } from 'features/nodes/types/common'; import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; /** * Gets the board field, based on the autoAddBoardId setting. @@ -22,25 +23,31 @@ export const getPresetModifiedPrompts = ( ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = state.controlLayers.present; - const { activeStylePreset } = state.stylePreset; + const { activeStylePresetId } = state.stylePreset; - if (activeStylePreset) { - const presetModifiedPositivePrompt = buildPresetModifiedPrompt( - activeStylePreset.preset_data.positive_prompt, - positivePrompt - ); + if (activeStylePresetId) { + const { data } = stylePresetsApi.endpoints.listStylePresets.select()(state); - const presetModifiedNegativePrompt = buildPresetModifiedPrompt( - activeStylePreset.preset_data.negative_prompt, - negativePrompt - ); + const activeStylePreset = data?.find((item) => item.id === activeStylePresetId); - return { - positivePrompt: presetModifiedPositivePrompt, - negativePrompt: presetModifiedNegativePrompt, - positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2, - negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2, - }; + if (activeStylePreset) { + const presetModifiedPositivePrompt = buildPresetModifiedPrompt( + activeStylePreset.preset_data.positive_prompt, + positivePrompt + ); + + const presetModifiedNegativePrompt = buildPresetModifiedPrompt( + activeStylePreset.preset_data.negative_prompt, + negativePrompt + ); + + return { + positivePrompt: presetModifiedPositivePrompt, + negativePrompt: presetModifiedNegativePrompt, + positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2, + negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2, + }; + } } return { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 0ab9f3c8f5..035dc417a7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -8,14 +8,24 @@ import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; - -const DEFAULT_HEIGHT = 20; +import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); - const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset); + const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + + const { activeStylePreset } = useListStylePresetsQuery(undefined, { + selectFromResult: ({ data }) => { + let activeStylePreset = null; + if (data) { + activeStylePreset = data.find((sp) => sp.id === activeStylePresetId); + } + return { activeStylePreset }; + }, + }); + const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( @@ -24,32 +34,18 @@ export const ParamNegativePrompt = memo(() => { }, [dispatch] ); - const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocusCursorAtEnd } = usePrompt({ + const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, onChange: _onChange, }); - const handleFocus = useCallback(() => { - setTimeout(() => { - onFocusCursorAtEnd(); - }, 500); - }, [onFocusCursorAtEnd]); - - if (viewMode) { - return ( - - ); - } - return ( + {viewMode && ( + + )}