mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into fix/inpaint_gen
This commit is contained in:
@ -17,8 +17,8 @@ from shutil import get_terminal_size
|
||||
from curses import BUTTON2_CLICKED, BUTTON3_CLICKED
|
||||
|
||||
# minimum size for UIs
|
||||
MIN_COLS = 130
|
||||
MIN_LINES = 38
|
||||
MIN_COLS = 150
|
||||
MIN_LINES = 40
|
||||
|
||||
|
||||
class WindowTooSmallException(Exception):
|
||||
@ -177,6 +177,8 @@ class FloatTitleSlider(npyscreen.TitleText):
|
||||
|
||||
|
||||
class SelectColumnBase:
|
||||
"""Base class for selection widget arranged in columns."""
|
||||
|
||||
def make_contained_widgets(self):
|
||||
self._my_widgets = []
|
||||
column_width = self.width // self.columns
|
||||
@ -253,6 +255,7 @@ class MultiSelectColumns(SelectColumnBase, npyscreen.MultiSelect):
|
||||
class SingleSelectWithChanged(npyscreen.SelectOne):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.on_changed = None
|
||||
|
||||
def h_select(self, ch):
|
||||
super().h_select(ch)
|
||||
@ -260,7 +263,9 @@ class SingleSelectWithChanged(npyscreen.SelectOne):
|
||||
self.on_changed(self.value)
|
||||
|
||||
|
||||
class SingleSelectColumns(SelectColumnBase, SingleSelectWithChanged):
|
||||
class SingleSelectColumnsSimple(SelectColumnBase, SingleSelectWithChanged):
|
||||
"""Row of radio buttons. Spacebar to select."""
|
||||
|
||||
def __init__(self, screen, columns: int = 1, values: list = [], **keywords):
|
||||
self.columns = columns
|
||||
self.value_cnt = len(values)
|
||||
@ -268,15 +273,19 @@ class SingleSelectColumns(SelectColumnBase, SingleSelectWithChanged):
|
||||
self.on_changed = None
|
||||
super().__init__(screen, values=values, **keywords)
|
||||
|
||||
def when_value_edited(self):
|
||||
self.h_select(self.cursor_line)
|
||||
def h_cursor_line_right(self, ch):
|
||||
self.h_exit_down("bye bye")
|
||||
|
||||
def h_cursor_line_left(self, ch):
|
||||
self.h_exit_up("bye bye")
|
||||
|
||||
|
||||
class SingleSelectColumns(SingleSelectColumnsSimple):
|
||||
"""Row of radio buttons. When tabbing over a selection, it is auto selected."""
|
||||
|
||||
def when_cursor_moved(self):
|
||||
self.h_select(self.cursor_line)
|
||||
|
||||
def h_cursor_line_right(self, ch):
|
||||
self.h_exit_down("bye bye")
|
||||
|
||||
|
||||
class TextBoxInner(npyscreen.MultiLineEdit):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -324,55 +333,6 @@ class TextBoxInner(npyscreen.MultiLineEdit):
|
||||
if bstate & (BUTTON2_CLICKED | BUTTON3_CLICKED):
|
||||
self.h_paste()
|
||||
|
||||
# def update(self, clear=True):
|
||||
# if clear:
|
||||
# self.clear()
|
||||
|
||||
# HEIGHT = self.height
|
||||
# WIDTH = self.width
|
||||
# # draw box.
|
||||
# self.parent.curses_pad.hline(self.rely, self.relx, curses.ACS_HLINE, WIDTH)
|
||||
# self.parent.curses_pad.hline(
|
||||
# self.rely + HEIGHT, self.relx, curses.ACS_HLINE, WIDTH
|
||||
# )
|
||||
# self.parent.curses_pad.vline(
|
||||
# self.rely, self.relx, curses.ACS_VLINE, self.height
|
||||
# )
|
||||
# self.parent.curses_pad.vline(
|
||||
# self.rely, self.relx + WIDTH, curses.ACS_VLINE, HEIGHT
|
||||
# )
|
||||
|
||||
# # draw corners
|
||||
# self.parent.curses_pad.addch(
|
||||
# self.rely,
|
||||
# self.relx,
|
||||
# curses.ACS_ULCORNER,
|
||||
# )
|
||||
# self.parent.curses_pad.addch(
|
||||
# self.rely,
|
||||
# self.relx + WIDTH,
|
||||
# curses.ACS_URCORNER,
|
||||
# )
|
||||
# self.parent.curses_pad.addch(
|
||||
# self.rely + HEIGHT,
|
||||
# self.relx,
|
||||
# curses.ACS_LLCORNER,
|
||||
# )
|
||||
# self.parent.curses_pad.addch(
|
||||
# self.rely + HEIGHT,
|
||||
# self.relx + WIDTH,
|
||||
# curses.ACS_LRCORNER,
|
||||
# )
|
||||
|
||||
# # fool our superclass into thinking drawing area is smaller - this is really hacky but it seems to work
|
||||
# (relx, rely, height, width) = (self.relx, self.rely, self.height, self.width)
|
||||
# self.relx += 1
|
||||
# self.rely += 1
|
||||
# self.height -= 1
|
||||
# self.width -= 1
|
||||
# super().update(clear=False)
|
||||
# (self.relx, self.rely, self.height, self.width) = (relx, rely, height, width)
|
||||
|
||||
|
||||
class TextBox(npyscreen.BoxTitle):
|
||||
_contained_widget = TextBoxInner
|
||||
|
@ -9,8 +9,8 @@ module.exports = {
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
@ -23,6 +23,11 @@ module.exports = {
|
||||
plugins: ['react', '@typescript-eslint', 'eslint-plugin-react-hooks'],
|
||||
root: true,
|
||||
rules: {
|
||||
curly: 'error',
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' },
|
||||
],
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
'no-var': 'error',
|
||||
'brace-style': 'error',
|
||||
@ -34,7 +39,6 @@ module.exports = {
|
||||
'warn',
|
||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||
],
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
|
@ -29,12 +29,13 @@
|
||||
"lint:eslint": "eslint --max-warnings=0 .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:tsc && yarn run lint:madge",
|
||||
"lint": "concurrently -g -n eslint,prettier,tsc,madge -c cyan,green,magenta,yellow \"yarn run lint:eslint\" \"yarn run lint:prettier\" \"yarn run lint:tsc\" \"yarn run lint:madge\"",
|
||||
"fix": "eslint --fix . && prettier --loglevel warn --write . && tsc --noEmit",
|
||||
"lint-staged": "lint-staged",
|
||||
"postinstall": "patch-package && yarn run theme",
|
||||
"theme": "chakra-cli tokens src/theme/theme.ts",
|
||||
"theme:watch": "chakra-cli tokens src/theme/theme.ts --watch"
|
||||
"theme:watch": "chakra-cli tokens src/theme/theme.ts --watch",
|
||||
"up": "yarn upgrade-interactive --latest"
|
||||
},
|
||||
"madge": {
|
||||
"detectiveOptions": {
|
||||
@ -54,7 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.2.0",
|
||||
"@chakra-ui/icons": "^2.0.19",
|
||||
"@chakra-ui/icons": "^2.1.0",
|
||||
"@chakra-ui/react": "^2.8.0",
|
||||
"@chakra-ui/styled-system": "^2.9.1",
|
||||
"@chakra-ui/theme-tools": "^2.1.0",
|
||||
@ -65,55 +66,55 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@floating-ui/react-dom": "^2.0.1",
|
||||
"@fontsource-variable/inter": "^5.0.3",
|
||||
"@fontsource/inter": "^5.0.3",
|
||||
"@mantine/core": "^6.0.14",
|
||||
"@mantine/form": "^6.0.15",
|
||||
"@mantine/hooks": "^6.0.14",
|
||||
"@fontsource-variable/inter": "^5.0.8",
|
||||
"@fontsource/inter": "^5.0.8",
|
||||
"@mantine/core": "^6.0.19",
|
||||
"@mantine/form": "^6.0.19",
|
||||
"@mantine/hooks": "^6.0.19",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@roarr/browser-log-writer": "^1.1.5",
|
||||
"chakra-ui-contextmenu": "^1.0.5",
|
||||
"dateformat": "^5.0.3",
|
||||
"downshift": "^7.6.0",
|
||||
"formik": "^2.4.2",
|
||||
"framer-motion": "^10.12.17",
|
||||
"formik": "^2.4.3",
|
||||
"framer-motion": "^10.16.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"i18next": "^23.2.3",
|
||||
"i18next": "^23.4.4",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"konva": "^9.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanostores": "^0.9.2",
|
||||
"openapi-fetch": "^0.6.1",
|
||||
"new-github-issue-url": "^1.0.0",
|
||||
"openapi-fetch": "^0.7.4",
|
||||
"overlayscrollbars": "^2.2.0",
|
||||
"overlayscrollbars-react": "^0.5.0",
|
||||
"patch-package": "^7.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"query-string": "^8.1.0",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hotkeys-hook": "4.4.0",
|
||||
"react-i18next": "^13.0.1",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hotkeys-hook": "4.4.1",
|
||||
"react-i18next": "^13.1.2",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-resizable-panels": "^0.0.52",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "^4.3.11",
|
||||
"react-virtuoso": "^4.5.0",
|
||||
"react-zoom-pan-pinch": "^3.0.8",
|
||||
"reactflow": "^11.7.4",
|
||||
"reactflow": "^11.8.3",
|
||||
"redux-dynamic-middlewares": "^2.2.0",
|
||||
"redux-remember": "^3.3.1",
|
||||
"roarr": "^7.15.0",
|
||||
"serialize-error": "^11.0.0",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"redux-remember": "^4.0.1",
|
||||
"roarr": "^7.15.1",
|
||||
"serialize-error": "^11.0.1",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"use-image": "^1.1.1",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.21.4"
|
||||
"zod": "^3.22.2",
|
||||
"zod-validation-error": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.0",
|
||||
@ -126,38 +127,36 @@
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/lodash-es": "^4.14.194",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/node": "^20.5.1",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"axios": "^1.4.0",
|
||||
"babel-plugin-transform-imports": "^2.0.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"form-data": "^4.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"lint-staged": "^14.0.1",
|
||||
"madge": "^6.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"openapi-typescript": "^6.2.8",
|
||||
"openapi-typescript-codegen": "^0.24.0",
|
||||
"openapi-typescript": "^6.5.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "^3.0.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"terser": "^5.18.1",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-css-injected-by-js": "^3.1.1",
|
||||
"vite-plugin-dts": "^2.3.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-css-injected-by-js": "^3.3.0",
|
||||
"vite-plugin-dts": "^3.5.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"yarn": "^1.22.19"
|
||||
|
@ -19,7 +19,7 @@
|
||||
"toggleAutoscroll": "Toggle autoscroll",
|
||||
"toggleLogViewer": "Toggle Log Viewer",
|
||||
"showGallery": "Show Gallery",
|
||||
"showOptionsPanel": "Show Options Panel",
|
||||
"showOptionsPanel": "Show Side Panel",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"common": {
|
||||
@ -52,7 +52,7 @@
|
||||
"img2img": "Image To Image",
|
||||
"unifiedCanvas": "Unified Canvas",
|
||||
"linear": "Linear",
|
||||
"nodes": "Node Editor",
|
||||
"nodes": "Workflow Editor",
|
||||
"batch": "Batch Manager",
|
||||
"modelManager": "Model Manager",
|
||||
"postprocessing": "Post Processing",
|
||||
@ -95,7 +95,6 @@
|
||||
"statusModelConverted": "Model Converted",
|
||||
"statusMergingModels": "Merging Models",
|
||||
"statusMergedModels": "Models Merged",
|
||||
"pinOptionsPanel": "Pin Options Panel",
|
||||
"loading": "Loading",
|
||||
"loadingInvokeAI": "Loading Invoke AI",
|
||||
"random": "Random",
|
||||
@ -116,7 +115,6 @@
|
||||
"maintainAspectRatio": "Maintain Aspect Ratio",
|
||||
"autoSwitchNewImages": "Auto-Switch to New Images",
|
||||
"singleColumnLayout": "Single Column Layout",
|
||||
"pinGallery": "Pin Gallery",
|
||||
"allImagesLoaded": "All Images Loaded",
|
||||
"loadMore": "Load More",
|
||||
"noImagesInGallery": "No Images to Display",
|
||||
@ -133,6 +131,7 @@
|
||||
"generalHotkeys": "General Hotkeys",
|
||||
"galleryHotkeys": "Gallery Hotkeys",
|
||||
"unifiedCanvasHotkeys": "Unified Canvas Hotkeys",
|
||||
"nodesHotkeys": "Nodes Hotkeys",
|
||||
"invoke": {
|
||||
"title": "Invoke",
|
||||
"desc": "Generate an image"
|
||||
@ -332,6 +331,10 @@
|
||||
"acceptStagingImage": {
|
||||
"title": "Accept Staging Image",
|
||||
"desc": "Accept Current Staging Area Image"
|
||||
},
|
||||
"addNodes": {
|
||||
"title": "Add Nodes",
|
||||
"desc": "Opens the add node menu"
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
@ -506,12 +509,9 @@
|
||||
"maskAdjustmentsHeader": "Mask Adjustments",
|
||||
"maskBlur": "Mask Blur",
|
||||
"maskBlurMethod": "Mask Blur Method",
|
||||
"seamPaintingHeader": "Seam Painting",
|
||||
"seamSize": "Seam Size",
|
||||
"seamBlur": "Seam Blur",
|
||||
"seamSteps": "Seam Steps",
|
||||
"seamStrength": "Seam Strength",
|
||||
"seamThreshold": "Seam Threshold",
|
||||
"coherencePassHeader": "Coherence Pass",
|
||||
"coherenceSteps": "Coherence Pass Steps",
|
||||
"coherenceStrength": "Coherence Pass Strength",
|
||||
"seamLowThreshold": "Low",
|
||||
"seamHighThreshold": "High",
|
||||
"scaleBeforeProcessing": "Scale Before Processing",
|
||||
@ -572,7 +572,7 @@
|
||||
"resetWebUI": "Reset Web UI",
|
||||
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
|
||||
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
|
||||
"resetComplete": "Web UI has been reset. Refresh the page to reload.",
|
||||
"resetComplete": "Web UI has been reset.",
|
||||
"consoleLogLevel": "Log Level",
|
||||
"shouldLogToConsole": "Console Logging",
|
||||
"developer": "Developer",
|
||||
@ -715,11 +715,12 @@
|
||||
"swapSizes": "Swap Sizes"
|
||||
},
|
||||
"nodes": {
|
||||
"reloadSchema": "Reload Schema",
|
||||
"saveGraph": "Save Graph",
|
||||
"loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)",
|
||||
"clearGraph": "Clear Graph",
|
||||
"clearGraphDesc": "Are you sure you want to clear all nodes?",
|
||||
"reloadNodeTemplates": "Reload Node Templates",
|
||||
"saveWorkflow": "Save Workflow",
|
||||
"loadWorkflow": "Load Workflow",
|
||||
"resetWorkflow": "Reset Workflow",
|
||||
"resetWorkflowDesc": "Are you sure you want to reset this workflow?",
|
||||
"resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.",
|
||||
"zoomInNodes": "Zoom In",
|
||||
"zoomOutNodes": "Zoom Out",
|
||||
"fitViewportNodes": "Fit View",
|
||||
|
@ -27,22 +27,10 @@ async function main() {
|
||||
* field accepts connection input. If it does, we can make the field optional.
|
||||
*/
|
||||
|
||||
// Check if we are generating types for an invocation
|
||||
const isInvocationPath = metadata.path.match(
|
||||
/^#\/components\/schemas\/\w*Invocation$/
|
||||
);
|
||||
|
||||
const hasInvocationProperties =
|
||||
schemaObject.properties &&
|
||||
['id', 'is_intermediate', 'type'].every(
|
||||
(prop) => prop in schemaObject.properties
|
||||
);
|
||||
|
||||
if (isInvocationPath && hasInvocationProperties) {
|
||||
if ('class' in schemaObject && schemaObject.class === 'invocation') {
|
||||
// We only want to make fields optional if they are required
|
||||
if (!Array.isArray(schemaObject?.required)) {
|
||||
schemaObject.required = ['id', 'type'];
|
||||
return;
|
||||
schemaObject.required = [];
|
||||
}
|
||||
|
||||
schemaObject.required.forEach((prop) => {
|
||||
@ -61,19 +49,13 @@ async function main() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
schemaObject.required = [
|
||||
...new Set(schemaObject.required.concat(['id', 'type'])),
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
// if (
|
||||
// 'input' in schemaObject &&
|
||||
// (schemaObject.input === 'any' || schemaObject.input === 'connection')
|
||||
// ) {
|
||||
// schemaObject.required = false;
|
||||
// }
|
||||
|
||||
// Check if we are generating types for an invocation output
|
||||
if ('class' in schemaObject && schemaObject.class === 'output') {
|
||||
// modify output types
|
||||
}
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(OUTPUT_FILE, types);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, Grid, Portal } from '@chakra-ui/react';
|
||||
import { Flex, Grid } from '@chakra-ui/react';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
@ -6,17 +6,15 @@ import { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploader from 'common/components/ImageUploader';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
||||
import SiteHeader from 'features/system/components/SiteHeader';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
||||
import i18n from 'i18n';
|
||||
import { size } from 'lodash-es';
|
||||
import { ReactNode, memo, useEffect } from 'react';
|
||||
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
import Toaster from './Toaster';
|
||||
|
||||
@ -30,8 +28,13 @@ interface Props {
|
||||
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
const language = useAppSelector(languageSelector);
|
||||
|
||||
const logger = useLogger();
|
||||
const logger = useLogger('system');
|
||||
const dispatch = useAppDispatch();
|
||||
const handleReset = useCallback(() => {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
@ -39,7 +42,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (size(config)) {
|
||||
logger.info({ namespace: 'App', config }, 'Received config');
|
||||
logger.info({ config }, 'Received config');
|
||||
dispatch(configChanged(config));
|
||||
}
|
||||
}, [dispatch, config, logger]);
|
||||
@ -49,7 +52,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
onReset={handleReset}
|
||||
FallbackComponent={AppErrorBoundaryFallback}
|
||||
>
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
<ImageUploader>
|
||||
<Grid
|
||||
@ -73,21 +79,12 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ImageUploader>
|
||||
|
||||
<GalleryDrawer />
|
||||
<ParametersDrawer />
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
<DeleteImageModal />
|
||||
<ChangeBoardModal />
|
||||
<Toaster />
|
||||
<GlobalHotkeys />
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,97 @@
|
||||
import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import newGithubIssueUrl from 'new-github-issue-url';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa';
|
||||
import { FaArrowRotateLeft } from 'react-icons/fa6';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
type Props = {
|
||||
error: Error;
|
||||
resetErrorBoundary: () => void;
|
||||
};
|
||||
|
||||
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
|
||||
const toast = useToast();
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const text = JSON.stringify(serializeError(error), null, 2);
|
||||
navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``);
|
||||
toast({
|
||||
title: 'Error Copied',
|
||||
});
|
||||
}, [error, toast]);
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
newGithubIssueUrl({
|
||||
user: 'invoke-ai',
|
||||
repo: 'InvokeAI',
|
||||
template: 'BUG_REPORT.yml',
|
||||
title: `[bug]: ${error.name}: ${error.message}`,
|
||||
}),
|
||||
[error.message, error.name]
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="body"
|
||||
sx={{
|
||||
w: '100vw',
|
||||
h: '100vh',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
borderRadius: 'base',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
p: 16,
|
||||
}}
|
||||
>
|
||||
<Heading>Something went wrong</Heading>
|
||||
<Flex
|
||||
layerStyle="second"
|
||||
sx={{
|
||||
px: 8,
|
||||
py: 4,
|
||||
borderRadius: 'base',
|
||||
gap: 4,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'error.500',
|
||||
_dark: { color: 'error.400' },
|
||||
}}
|
||||
>
|
||||
{error.name}: {error.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex sx={{ gap: 4 }}>
|
||||
<IAIButton
|
||||
leftIcon={<FaArrowRotateLeft />}
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
Reset UI
|
||||
</IAIButton>
|
||||
<IAIButton leftIcon={<FaCopy />} onClick={handleCopy}>
|
||||
Copy Error
|
||||
</IAIButton>
|
||||
<Link href={url} isExternal>
|
||||
<IAIButton leftIcon={<FaExternalLinkAlt />}>Create Issue</IAIButton>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AppErrorBoundaryFallback);
|
@ -1,30 +1,21 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import {
|
||||
ctrlKeyPressed,
|
||||
metaKeyPressed,
|
||||
shiftKeyPressed,
|
||||
} from 'features/ui/store/hotkeysSlice';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import {
|
||||
setActiveTab,
|
||||
toggleGalleryPanel,
|
||||
toggleParametersPanel,
|
||||
togglePinGalleryPanel,
|
||||
togglePinParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import React, { memo } from 'react';
|
||||
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const globalHotkeysSelector = createSelector(
|
||||
[stateSelector],
|
||||
({ hotkeys, ui }) => {
|
||||
({ hotkeys }) => {
|
||||
const { shift, ctrl, meta } = hotkeys;
|
||||
const { shouldPinParametersPanel, shouldPinGallery } = ui;
|
||||
return { shift, ctrl, meta, shouldPinGallery, shouldPinParametersPanel };
|
||||
return { shift, ctrl, meta };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
@ -41,9 +32,7 @@ const globalHotkeysSelector = createSelector(
|
||||
*/
|
||||
const GlobalHotkeys: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { shift, ctrl, meta, shouldPinParametersPanel, shouldPinGallery } =
|
||||
useAppSelector(globalHotkeysSelector);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const { shift, ctrl, meta } = useAppSelector(globalHotkeysSelector);
|
||||
|
||||
useHotkeys(
|
||||
'*',
|
||||
@ -68,34 +57,6 @@ const GlobalHotkeys: React.FC = () => {
|
||||
[shift, ctrl, meta]
|
||||
);
|
||||
|
||||
useHotkeys('o', () => {
|
||||
dispatch(toggleParametersPanel());
|
||||
if (activeTabName === 'unifiedCanvas' && shouldPinParametersPanel) {
|
||||
dispatch(requestCanvasRescale());
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(['shift+o'], () => {
|
||||
dispatch(togglePinParametersPanel());
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
dispatch(requestCanvasRescale());
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys('g', () => {
|
||||
dispatch(toggleGalleryPanel());
|
||||
if (activeTabName === 'unifiedCanvas' && shouldPinGallery) {
|
||||
dispatch(requestCanvasRescale());
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(['shift+g'], () => {
|
||||
dispatch(togglePinGalleryPanel());
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
dispatch(requestCanvasRescale());
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys('1', () => {
|
||||
dispatch(setActiveTab('txt2img'));
|
||||
});
|
||||
@ -112,6 +73,10 @@ const GlobalHotkeys: React.FC = () => {
|
||||
dispatch(setActiveTab('nodes'));
|
||||
});
|
||||
|
||||
useHotkeys('5', () => {
|
||||
dispatch(setActiveTab('modelManager'));
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
createLocalStorageManager,
|
||||
extendTheme,
|
||||
} from '@chakra-ui/react';
|
||||
import { ReactNode, useEffect, useMemo } from 'react';
|
||||
import { ReactNode, memo, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { theme as invokeAITheme } from 'theme/theme';
|
||||
|
||||
@ -46,4 +46,4 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeLocaleProvider;
|
||||
export default memo(ThemeLocaleProvider);
|
||||
|
@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { toastQueueSelector } from 'features/system/store/systemSelectors';
|
||||
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
|
||||
import { MakeToastArg, makeToast } from 'features/system/util/makeToast';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Logical component. Watches the toast queue and makes toasts when the queue is not empty.
|
||||
@ -44,4 +44,4 @@ export const useAppToaster = () => {
|
||||
return toaster;
|
||||
};
|
||||
|
||||
export default Toaster;
|
||||
export default memo(Toaster);
|
||||
|
@ -9,7 +9,7 @@ export const log = Roarr.child(BASE_CONTEXT);
|
||||
|
||||
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
|
||||
|
||||
type LoggerNamespace =
|
||||
export type LoggerNamespace =
|
||||
| 'images'
|
||||
| 'models'
|
||||
| 'config'
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createLogWriter } from '@roarr/browser-log-writer';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { ROARR, Roarr } from 'roarr';
|
||||
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP } from './logger';
|
||||
import {
|
||||
$logger,
|
||||
BASE_CONTEXT,
|
||||
LOG_LEVEL_MAP,
|
||||
LoggerNamespace,
|
||||
logger,
|
||||
} from './logger';
|
||||
|
||||
const selector = createSelector(
|
||||
systemSelector,
|
||||
@ -25,7 +30,7 @@ const selector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const useLogger = () => {
|
||||
export const useLogger = (namespace: LoggerNamespace) => {
|
||||
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
|
||||
|
||||
// The provided Roarr browser log writer uses localStorage to config logging to console
|
||||
@ -57,7 +62,7 @@ export const useLogger = () => {
|
||||
$logger.set(Roarr.child(newContext));
|
||||
}, []);
|
||||
|
||||
const logger = useStore($logger);
|
||||
const log = useMemo(() => logger(namespace), [namespace]);
|
||||
|
||||
return logger;
|
||||
return log;
|
||||
};
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||
import {
|
||||
controlNetImageChanged,
|
||||
controlNetProcessedImageChanged,
|
||||
} from 'features/controlNet/store/controlNetSlice';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/types';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { clamp, forEach } from 'lodash-es';
|
||||
import { api } from 'services/api';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesAdapter } from 'services/api/util';
|
||||
@ -73,22 +77,61 @@ export const addRequestedSingleImageDeletionListener = () => {
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
|
||||
if (imageUsage.isCanvasImage) {
|
||||
dispatch(resetCanvas());
|
||||
}
|
||||
|
||||
if (imageUsage.isControlNetImage) {
|
||||
dispatch(controlNetReset());
|
||||
}
|
||||
imageDTOs.forEach((imageDTO) => {
|
||||
// reset init image if we deleted it
|
||||
if (
|
||||
getState().generation.initialImage?.imageName === imageDTO.image_name
|
||||
) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
||||
if (imageUsage.isInitialImage) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
// reset controlNets that use the deleted images
|
||||
forEach(getState().controlNet.controlNets, (controlNet) => {
|
||||
if (
|
||||
controlNet.controlImage === imageDTO.image_name ||
|
||||
controlNet.processedControlImage === imageDTO.image_name
|
||||
) {
|
||||
dispatch(
|
||||
controlNetImageChanged({
|
||||
controlNetId: controlNet.controlNetId,
|
||||
controlImage: null,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
controlNetProcessedImageChanged({
|
||||
controlNetId: controlNet.controlNetId,
|
||||
processedControlImage: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (imageUsage.isNodesImage) {
|
||||
dispatch(nodeEditorReset());
|
||||
}
|
||||
// reset nodes that use the deleted images
|
||||
getState().nodes.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (
|
||||
input.type === 'ImageField' &&
|
||||
input.value?.image_name === imageDTO.image_name
|
||||
) {
|
||||
dispatch(
|
||||
fieldImageValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete from server
|
||||
const { requestId } = dispatch(
|
||||
@ -154,17 +197,58 @@ export const addRequestedMultipleImageDeletionListener = () => {
|
||||
dispatch(resetCanvas());
|
||||
}
|
||||
|
||||
if (imagesUsage.some((i) => i.isControlNetImage)) {
|
||||
dispatch(controlNetReset());
|
||||
}
|
||||
imageDTOs.forEach((imageDTO) => {
|
||||
// reset init image if we deleted it
|
||||
if (
|
||||
getState().generation.initialImage?.imageName ===
|
||||
imageDTO.image_name
|
||||
) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
||||
if (imagesUsage.some((i) => i.isInitialImage)) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
// reset controlNets that use the deleted images
|
||||
forEach(getState().controlNet.controlNets, (controlNet) => {
|
||||
if (
|
||||
controlNet.controlImage === imageDTO.image_name ||
|
||||
controlNet.processedControlImage === imageDTO.image_name
|
||||
) {
|
||||
dispatch(
|
||||
controlNetImageChanged({
|
||||
controlNetId: controlNet.controlNetId,
|
||||
controlImage: null,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
controlNetProcessedImageChanged({
|
||||
controlNetId: controlNet.controlNetId,
|
||||
processedControlImage: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (imagesUsage.some((i) => i.isNodesImage)) {
|
||||
dispatch(nodeEditorReset());
|
||||
}
|
||||
// reset nodes that use the deleted images
|
||||
getState().nodes.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (
|
||||
input.type === 'ImageField' &&
|
||||
input.value?.image_name === imageDTO.image_name
|
||||
) {
|
||||
dispatch(
|
||||
fieldImageValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { modelsApi } from 'services/api/endpoints/models';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import { appSocketConnected, socketConnected } from 'services/events/actions';
|
||||
import { startAppListening } from '../..';
|
||||
import { size } from 'lodash-es';
|
||||
|
||||
export const addSocketConnectedEventListener = () => {
|
||||
startAppListening({
|
||||
@ -18,7 +19,7 @@ export const addSocketConnectedEventListener = () => {
|
||||
|
||||
const { disabledTabs } = config;
|
||||
|
||||
if (!nodes.schema && !disabledTabs.includes('nodes')) {
|
||||
if (!size(nodes.nodeTemplates) && !disabledTabs.includes('nodes')) {
|
||||
dispatch(receivedOpenAPISchema());
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
export interface IAIButtonProps extends ButtonProps {
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
tooltip?: TooltipProps['label'];
|
||||
tooltipProps?: Omit<TooltipProps, 'children' | 'label'>;
|
||||
isChecked?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
@ -34,14 +34,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
||||
gap: 2,
|
||||
borderTopRadius: 'base',
|
||||
borderBottomRadius: isOpen ? 0 : 'base',
|
||||
bg: isOpen
|
||||
? mode('base.200', 'base.750')(colorMode)
|
||||
: mode('base.150', 'base.800')(colorMode),
|
||||
bg: mode('base.250', 'base.750')(colorMode),
|
||||
color: mode('base.900', 'base.100')(colorMode),
|
||||
_hover: {
|
||||
bg: isOpen
|
||||
? mode('base.250', 'base.700')(colorMode)
|
||||
: mode('base.200', 'base.750')(colorMode),
|
||||
bg: mode('base.300', 'base.700')(colorMode),
|
||||
},
|
||||
fontSize: 'sm',
|
||||
fontWeight: 600,
|
||||
@ -90,9 +86,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
||||
<Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 4,
|
||||
p: 2,
|
||||
pt: 3,
|
||||
borderBottomRadius: 'base',
|
||||
bg: 'base.100',
|
||||
bg: 'base.150',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
|
@ -100,14 +100,18 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (onMouseOver) onMouseOver(e);
|
||||
if (onMouseOver) {
|
||||
onMouseOver(e);
|
||||
}
|
||||
setIsHovered(true);
|
||||
},
|
||||
[onMouseOver]
|
||||
);
|
||||
const handleMouseOut = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (onMouseOut) onMouseOut(e);
|
||||
if (onMouseOut) {
|
||||
onMouseOut(e);
|
||||
}
|
||||
setIsHovered(false);
|
||||
},
|
||||
[onMouseOut]
|
||||
@ -122,7 +126,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
? {}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
bg: mode('base.200', 'base.800')(colorMode),
|
||||
bg: mode('base.200', 'base.700')(colorMode),
|
||||
_hover: {
|
||||
bg: mode('base.300', 'base.650')(colorMode),
|
||||
color: mode('base.500', 'base.300')(colorMode),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Box, Flex, Icon } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
const IAIErrorLoadingImageFallback = () => {
|
||||
@ -39,4 +40,4 @@ const IAIErrorLoadingImageFallback = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIErrorLoadingImageFallback;
|
||||
export default memo(IAIErrorLoadingImageFallback);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Box, Skeleton } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const IAIFillSkeleton = () => {
|
||||
return (
|
||||
@ -27,4 +28,4 @@ const IAIFillSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIFillSkeleton;
|
||||
export default memo(IAIFillSkeleton);
|
||||
|
@ -9,8 +9,8 @@ import { memo } from 'react';
|
||||
|
||||
export type IAIIconButtonProps = IconButtonProps & {
|
||||
role?: string;
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
tooltip?: TooltipProps['label'];
|
||||
tooltipProps?: Omit<TooltipProps, 'children' | 'label'>;
|
||||
isChecked?: boolean;
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Badge, Flex } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
|
||||
type ImageMetadataOverlayProps = {
|
||||
@ -26,4 +27,4 @@ const ImageMetadataOverlay = ({ imageDTO }: ImageMetadataOverlayProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageMetadataOverlay;
|
||||
export default memo(ImageMetadataOverlay);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
type ImageUploadOverlayProps = {
|
||||
@ -87,4 +88,4 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default ImageUploadOverlay;
|
||||
export default memo(ImageUploadOverlay);
|
||||
|
@ -150,7 +150,9 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
{...getRootProps({ style: {} })}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
// Bail out if user hits spacebar - do not open the uploader
|
||||
if (e.key === ' ') return;
|
||||
if (e.key === ' ') {
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Flex, Icon } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
|
||||
const SelectImagePlaceholder = () => {
|
||||
@ -19,4 +20,4 @@ const SelectImagePlaceholder = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectImagePlaceholder;
|
||||
export default memo(SelectImagePlaceholder);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
isSelected: boolean;
|
||||
@ -18,6 +19,7 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
|
||||
opacity: isSelected ? 1 : 0.7,
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
pointerEvents: 'none',
|
||||
shadow: isSelected
|
||||
? isHovered
|
||||
? 'hoverSelected.light'
|
||||
@ -39,4 +41,4 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectionOverlay;
|
||||
export default memo(SelectionOverlay);
|
||||
|
@ -2,71 +2,108 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
// import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||
import { isInvocationNode } from 'features/nodes/types/types';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { NON_REFINER_BASE_MODELS } from 'services/api/constants';
|
||||
import { modelsApi } from '../../services/api/endpoints/models';
|
||||
import { forEach, map } from 'lodash-es';
|
||||
import { getConnectedEdges } from 'reactflow';
|
||||
|
||||
const readinessSelector = createSelector(
|
||||
const selector = createSelector(
|
||||
[stateSelector, activeTabNameSelector],
|
||||
(state, activeTabName) => {
|
||||
const { generation, system } = state;
|
||||
const { initialImage } = generation;
|
||||
const { generation, system, nodes } = state;
|
||||
const { initialImage, model } = generation;
|
||||
|
||||
const { isProcessing, isConnected } = system;
|
||||
|
||||
let isReady = true;
|
||||
const reasonsWhyNotReady: string[] = [];
|
||||
const reasons: string[] = [];
|
||||
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('No initial image selected');
|
||||
}
|
||||
|
||||
const { isSuccess: mainModelsSuccessfullyLoaded } =
|
||||
modelsApi.endpoints.getMainModels.select(NON_REFINER_BASE_MODELS)(state);
|
||||
if (!mainModelsSuccessfullyLoaded) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('Models are not loaded');
|
||||
}
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('System Busy');
|
||||
reasons.push('System busy');
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('System Disconnected');
|
||||
reasons.push('System disconnected');
|
||||
}
|
||||
|
||||
// // Cannot generate variations without valid seed weights
|
||||
// if (
|
||||
// shouldGenerateVariations &&
|
||||
// (!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
// ) {
|
||||
// isReady = false;
|
||||
// reasonsWhyNotReady.push('Seed-Weights badly formatted.');
|
||||
// }
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
reasons.push('No initial image selected');
|
||||
}
|
||||
|
||||
forEach(state.controlNet.controlNets, (controlNet, id) => {
|
||||
if (!controlNet.model) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push(`ControlNet ${id} has no model selected.`);
|
||||
if (activeTabName === 'nodes' && nodes.shouldValidateGraph) {
|
||||
if (!nodes.nodes.length) {
|
||||
reasons.push('No nodes in graph');
|
||||
}
|
||||
});
|
||||
|
||||
// All good
|
||||
return { isReady, reasonsWhyNotReady };
|
||||
nodes.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeTemplate = nodes.nodeTemplates[node.data.type];
|
||||
|
||||
if (!nodeTemplate) {
|
||||
// Node type not found
|
||||
reasons.push('Missing node template');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectedEdges = getConnectedEdges([node], nodes.edges);
|
||||
|
||||
forEach(node.data.inputs, (field) => {
|
||||
const fieldTemplate = nodeTemplate.inputs[field.name];
|
||||
const hasConnection = connectedEdges.some(
|
||||
(edge) =>
|
||||
edge.target === node.id && edge.targetHandle === field.name
|
||||
);
|
||||
|
||||
if (!fieldTemplate) {
|
||||
reasons.push('Missing field template');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldTemplate.required && !field.value && !hasConnection) {
|
||||
reasons.push(
|
||||
`${node.data.label || nodeTemplate.title} -> ${
|
||||
field.label || fieldTemplate.title
|
||||
} missing input`
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!model) {
|
||||
reasons.push('No model selected');
|
||||
}
|
||||
|
||||
if (state.controlNet.isEnabled) {
|
||||
map(state.controlNet.controlNets).forEach((controlNet, i) => {
|
||||
if (!controlNet.isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!controlNet.model) {
|
||||
reasons.push(`ControlNet ${i + 1} has no model selected.`);
|
||||
}
|
||||
|
||||
if (
|
||||
!controlNet.controlImage ||
|
||||
(!controlNet.processedControlImage &&
|
||||
controlNet.processorType !== 'none')
|
||||
) {
|
||||
reasons.push(`ControlNet ${i + 1} has no control image`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { isReady: !reasons.length, isProcessing, reasons };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const useIsReadyToInvoke = () => {
|
||||
const { isReady } = useAppSelector(readinessSelector);
|
||||
return isReady;
|
||||
const { isReady, isProcessing, reasons } = useAppSelector(selector);
|
||||
return { isReady, isProcessing, reasons };
|
||||
};
|
||||
|
@ -11,8 +11,14 @@ export default function useResolution():
|
||||
const tabletResolutions = ['md', 'lg'];
|
||||
const desktopResolutions = ['xl', '2xl'];
|
||||
|
||||
if (mobileResolutions.includes(breakpointValue)) return 'mobile';
|
||||
if (tabletResolutions.includes(breakpointValue)) return 'tablet';
|
||||
if (desktopResolutions.includes(breakpointValue)) return 'desktop';
|
||||
if (mobileResolutions.includes(breakpointValue)) {
|
||||
return 'mobile';
|
||||
}
|
||||
if (tabletResolutions.includes(breakpointValue)) {
|
||||
return 'tablet';
|
||||
}
|
||||
if (desktopResolutions.includes(breakpointValue)) {
|
||||
return 'desktop';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
export const colorTokenToCssVar = (colorToken: string) =>
|
||||
`var(--invokeai-colors-${colorToken.split('.').join('-')}`;
|
@ -6,7 +6,11 @@ export const dateComparator = (a: string, b: string) => {
|
||||
const dateB = new Date(b);
|
||||
|
||||
// sort in ascending order
|
||||
if (dateA > dateB) return 1;
|
||||
if (dateA < dateB) return -1;
|
||||
if (dateA > dateB) {
|
||||
return 1;
|
||||
}
|
||||
if (dateA < dateB) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
@ -5,7 +5,9 @@ type Base64AndCaption = {
|
||||
|
||||
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
|
||||
const w = window.open('');
|
||||
if (!w) return;
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
images.forEach((i) => {
|
||||
const image = new Image();
|
||||
|
@ -5,6 +5,7 @@ import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { isStagingSelector } from '../store/canvasSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
const ClearCanvasHistoryButtonModal = () => {
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
@ -28,4 +29,4 @@ const ClearCanvasHistoryButtonModal = () => {
|
||||
</IAIAlertDialog>
|
||||
);
|
||||
};
|
||||
export default ClearCanvasHistoryButtonModal;
|
||||
export default memo(ClearCanvasHistoryButtonModal);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Box, chakra, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import {
|
||||
canvasSelector,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import Konva from 'konva';
|
||||
import { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { Layer, Stage } from 'react-konva';
|
||||
import useCanvasDragMove from '../hooks/useCanvasDragMove';
|
||||
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
|
||||
@ -18,6 +18,7 @@ import useCanvasMouseMove from '../hooks/useCanvasMouseMove';
|
||||
import useCanvasMouseOut from '../hooks/useCanvasMouseOut';
|
||||
import useCanvasMouseUp from '../hooks/useCanvasMouseUp';
|
||||
import useCanvasWheel from '../hooks/useCanvasZoom';
|
||||
import { canvasResized } from '../store/canvasSlice';
|
||||
import {
|
||||
setCanvasBaseLayer,
|
||||
setCanvasStage,
|
||||
@ -106,7 +107,8 @@ const IAICanvas = () => {
|
||||
shouldAntialias,
|
||||
} = useAppSelector(selector);
|
||||
useCanvasHotkeys();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const stageRef = useRef<Konva.Stage | null>(null);
|
||||
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
||||
|
||||
@ -137,8 +139,30 @@ const IAICanvas = () => {
|
||||
const { handleDragStart, handleDragMove, handleDragEnd } =
|
||||
useCanvasDragMove();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.contentBoxSize) {
|
||||
const { width, height } = entry.contentRect;
|
||||
dispatch(canvasResized({ width, height }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id="canvas-container"
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
@ -146,13 +170,18 @@ const IAICanvas = () => {
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
// top: 0,
|
||||
// insetInlineStart: 0,
|
||||
}}
|
||||
>
|
||||
<ChakraStage
|
||||
tabIndex={-1}
|
||||
ref={canvasStageRefCallback}
|
||||
sx={{
|
||||
outline: 'none',
|
||||
// boxShadow: '0px 0px 0px 1px var(--border-color-light)',
|
||||
overflow: 'hidden',
|
||||
cursor: stageCursor ? stageCursor : undefined,
|
||||
canvas: {
|
||||
@ -213,11 +242,11 @@ const IAICanvas = () => {
|
||||
/>
|
||||
</Layer>
|
||||
</ChakraStage>
|
||||
<IAICanvasStatusText />
|
||||
<IAICanvasStagingAreaToolbar />
|
||||
</Box>
|
||||
<IAICanvasStatusText />
|
||||
<IAICanvasStagingAreaToolbar />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvas;
|
||||
export default memo(IAICanvas);
|
||||
|
@ -4,6 +4,7 @@ import { isEqual } from 'lodash-es';
|
||||
|
||||
import { Group, Rect } from 'react-konva';
|
||||
import { canvasSelector } from '../store/canvasSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
canvasSelector,
|
||||
@ -67,4 +68,4 @@ const IAICanvasBoundingBoxOverlay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasBoundingBoxOverlay;
|
||||
export default memo(IAICanvasBoundingBoxOverlay);
|
||||
|
@ -6,7 +6,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { isEqual, range } from 'lodash-es';
|
||||
|
||||
import { ReactNode, useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { ReactNode, memo, useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { Group, Line as KonvaLine } from 'react-konva';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -117,4 +117,4 @@ const IAICanvasGrid = () => {
|
||||
return <Group>{gridLines}</Group>;
|
||||
};
|
||||
|
||||
export default IAICanvasGrid;
|
||||
export default memo(IAICanvasGrid);
|
||||
|
@ -4,6 +4,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import useImage from 'use-image';
|
||||
import { CanvasImage } from '../store/canvasTypes';
|
||||
import { $authToken } from 'services/api/client';
|
||||
import { memo } from 'react';
|
||||
|
||||
type IAICanvasImageProps = {
|
||||
canvasImage: CanvasImage;
|
||||
@ -25,4 +26,4 @@ const IAICanvasImage = (props: IAICanvasImageProps) => {
|
||||
return <Image x={x} y={y} image={image} listening={false} />;
|
||||
};
|
||||
|
||||
export default IAICanvasImage;
|
||||
export default memo(IAICanvasImage);
|
||||
|
@ -4,7 +4,7 @@ import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { ImageConfig } from 'konva/lib/shapes/Image';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { Image as KonvaImage } from 'react-konva';
|
||||
import { canvasSelector } from '../store/canvasSelectors';
|
||||
|
||||
@ -66,4 +66,4 @@ const IAICanvasIntermediateImage = (props: Props) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default IAICanvasIntermediateImage;
|
||||
export default memo(IAICanvasIntermediateImage);
|
||||
|
@ -7,7 +7,7 @@ import { Rect } from 'react-konva';
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
import Konva from 'konva';
|
||||
import { isNumber } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const canvasMaskCompositerSelector = createSelector(
|
||||
canvasSelector,
|
||||
@ -125,7 +125,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
|
||||
}, [offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fillPatternImage) return;
|
||||
if (fillPatternImage) {
|
||||
return;
|
||||
}
|
||||
const image = new Image();
|
||||
|
||||
image.onload = () => {
|
||||
@ -135,7 +137,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
|
||||
}, [fillPatternImage, maskColorString]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fillPatternImage) return;
|
||||
if (!fillPatternImage) {
|
||||
return;
|
||||
}
|
||||
fillPatternImage.src = getColoredSVG(maskColorString);
|
||||
}, [fillPatternImage, maskColorString]);
|
||||
|
||||
@ -151,8 +155,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
|
||||
!isNumber(stageScale) ||
|
||||
!isNumber(stageDimensions.width) ||
|
||||
!isNumber(stageDimensions.height)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Rect
|
||||
@ -172,4 +177,4 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasMaskCompositer;
|
||||
export default memo(IAICanvasMaskCompositer);
|
||||
|
@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es';
|
||||
|
||||
import { Group, Line } from 'react-konva';
|
||||
import { isCanvasMaskLine } from '../store/canvasTypes';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const canvasLinesSelector = createSelector(
|
||||
[canvasSelector],
|
||||
@ -52,4 +53,4 @@ const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasLines;
|
||||
export default memo(IAICanvasLines);
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
isCanvasFillRect,
|
||||
} from '../store/canvasTypes';
|
||||
import IAICanvasImage from './IAICanvasImage';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
[canvasSelector],
|
||||
@ -33,7 +34,9 @@ const selector = createSelector(
|
||||
const IAICanvasObjectRenderer = () => {
|
||||
const { objects } = useAppSelector(selector);
|
||||
|
||||
if (!objects) return null;
|
||||
if (!objects) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group name="outpainting-objects" listening={false}>
|
||||
@ -101,4 +104,4 @@ const IAICanvasObjectRenderer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasObjectRenderer;
|
||||
export default memo(IAICanvasObjectRenderer);
|
||||
|
@ -1,89 +0,0 @@
|
||||
import { Flex, Spinner } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
canvasSelector,
|
||||
initialCanvasImageSelector,
|
||||
} from 'features/canvas/store/canvasSelectors';
|
||||
import {
|
||||
resizeAndScaleCanvas,
|
||||
resizeCanvas,
|
||||
setCanvasContainerDimensions,
|
||||
setDoesCanvasNeedScaling,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
const canvasResizerSelector = createSelector(
|
||||
canvasSelector,
|
||||
initialCanvasImageSelector,
|
||||
activeTabNameSelector,
|
||||
(canvas, initialCanvasImage, activeTabName) => {
|
||||
const { doesCanvasNeedScaling, isCanvasInitialized } = canvas;
|
||||
return {
|
||||
doesCanvasNeedScaling,
|
||||
activeTabName,
|
||||
initialCanvasImage,
|
||||
isCanvasInitialized,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const IAICanvasResizer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
doesCanvasNeedScaling,
|
||||
activeTabName,
|
||||
initialCanvasImage,
|
||||
isCanvasInitialized,
|
||||
} = useAppSelector(canvasResizerSelector);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { clientWidth, clientHeight } = ref.current;
|
||||
|
||||
dispatch(
|
||||
setCanvasContainerDimensions({
|
||||
width: clientWidth,
|
||||
height: clientHeight,
|
||||
})
|
||||
);
|
||||
|
||||
if (!isCanvasInitialized) {
|
||||
dispatch(resizeAndScaleCanvas());
|
||||
} else {
|
||||
dispatch(resizeCanvas());
|
||||
}
|
||||
|
||||
dispatch(setDoesCanvasNeedScaling(false));
|
||||
}, 0);
|
||||
}, [
|
||||
dispatch,
|
||||
initialCanvasImage,
|
||||
doesCanvasNeedScaling,
|
||||
activeTabName,
|
||||
isCanvasInitialized,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Spinner thickness="2px" size="xl" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasResizer;
|
@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es';
|
||||
|
||||
import { Group, Rect } from 'react-konva';
|
||||
import IAICanvasImage from './IAICanvasImage';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
[canvasSelector],
|
||||
@ -88,4 +89,4 @@ const IAICanvasStagingArea = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasStagingArea;
|
||||
export default memo(IAICanvasStagingArea);
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@ -129,7 +129,9 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
currentStagingAreaImage?.imageName ?? skipToken
|
||||
);
|
||||
|
||||
if (!currentStagingAreaImage) return null;
|
||||
if (!currentStagingAreaImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -138,11 +140,10 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
w="100%"
|
||||
align="center"
|
||||
justify="center"
|
||||
filter="drop-shadow(0 0.5rem 1rem rgba(0,0,0))"
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
>
|
||||
<ButtonGroup isAttached>
|
||||
<ButtonGroup isAttached borderRadius="base" shadow="dark-lg">
|
||||
<IAIIconButton
|
||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
@ -207,4 +208,4 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasStagingAreaToolbar;
|
||||
export default memo(IAICanvasStagingAreaToolbar);
|
||||
|
@ -7,6 +7,7 @@ import { isEqual } from 'lodash-es';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import roundToHundreth from '../util/roundToHundreth';
|
||||
import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos';
|
||||
import { memo } from 'react';
|
||||
|
||||
const warningColor = 'var(--invokeai-colors-warning-500)';
|
||||
|
||||
@ -162,4 +163,4 @@ const IAICanvasStatusText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasStatusText;
|
||||
export default memo(IAICanvasStatusText);
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
COLOR_PICKER_SIZE,
|
||||
COLOR_PICKER_STROKE_RADIUS,
|
||||
} from '../util/constants';
|
||||
import { memo } from 'react';
|
||||
|
||||
const canvasBrushPreviewSelector = createSelector(
|
||||
canvasSelector,
|
||||
@ -134,7 +135,9 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
|
||||
clip,
|
||||
} = useAppSelector(canvasBrushPreviewSelector);
|
||||
|
||||
if (!shouldDrawBrushPreview) return null;
|
||||
if (!shouldDrawBrushPreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group listening={false} {...clip} {...rest}>
|
||||
@ -206,4 +209,4 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasToolPreview;
|
||||
export default memo(IAICanvasToolPreview);
|
||||
|
@ -19,7 +19,7 @@ import { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Group, Rect, Transformer } from 'react-konva';
|
||||
|
||||
@ -85,7 +85,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !shapeRef.current) return;
|
||||
if (!transformerRef.current || !shapeRef.current) {
|
||||
return;
|
||||
}
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}, []);
|
||||
@ -133,7 +135,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
* not its width and height. We need to un-scale the width and height before
|
||||
* setting the values.
|
||||
*/
|
||||
if (!shapeRef.current) return;
|
||||
if (!shapeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = shapeRef.current;
|
||||
|
||||
@ -313,4 +317,4 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasBoundingBox;
|
||||
export default memo(IAICanvasBoundingBox);
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -150,4 +151,4 @@ const IAICanvasMaskOptions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasMaskOptions;
|
||||
export default memo(IAICanvasMaskOptions);
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { ChangeEvent } from 'react';
|
||||
import { ChangeEvent, memo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaWrench } from 'react-icons/fa';
|
||||
@ -163,4 +163,4 @@ const IAICanvasSettingsButtonPopover = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasSettingsButtonPopover;
|
||||
export default memo(IAICanvasSettingsButtonPopover);
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -252,4 +253,4 @@ const IAICanvasToolChooserOptions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasToolChooserOptions;
|
||||
export default memo(IAICanvasToolChooserOptions);
|
||||
|
@ -18,7 +18,6 @@ import {
|
||||
import {
|
||||
resetCanvas,
|
||||
resetCanvasView,
|
||||
resizeAndScaleCanvas,
|
||||
setIsMaskEnabled,
|
||||
setLayer,
|
||||
setTool,
|
||||
@ -48,6 +47,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
|
||||
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const selector = createSelector(
|
||||
[systemSelector, canvasSelector, isStagingSelector],
|
||||
@ -166,7 +166,9 @@ const IAICanvasToolbar = () => {
|
||||
|
||||
const handleResetCanvasView = (shouldScaleTo1 = false) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
if (!canvasBaseLayer) return;
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
const clientRect = canvasBaseLayer.getClientRect({
|
||||
skipTransform: true,
|
||||
});
|
||||
@ -180,7 +182,6 @@ const IAICanvasToolbar = () => {
|
||||
|
||||
const handleResetCanvas = () => {
|
||||
dispatch(resetCanvas());
|
||||
dispatch(resizeAndScaleCanvas());
|
||||
};
|
||||
|
||||
const handleMergeVisible = () => {
|
||||
@ -309,4 +310,4 @@ const IAICanvasToolbar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICanvasToolbar;
|
||||
export default memo(IAICanvasToolbar);
|
||||
|
@ -32,13 +32,17 @@ const useCanvasDrag = () => {
|
||||
|
||||
return {
|
||||
handleDragStart: useCallback(() => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
return;
|
||||
}
|
||||
dispatch(setIsMovingStage(true));
|
||||
}, [dispatch, isMovingBoundingBox, isStaging, tool]),
|
||||
|
||||
handleDragMove: useCallback(
|
||||
(e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCoordinates = { x: e.target.x(), y: e.target.y() };
|
||||
|
||||
@ -48,7 +52,9 @@ const useCanvasDrag = () => {
|
||||
),
|
||||
|
||||
handleDragEnd: useCallback(() => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
return;
|
||||
}
|
||||
dispatch(setIsMovingStage(false));
|
||||
}, [dispatch, isMovingBoundingBox, isStaging, tool]),
|
||||
};
|
||||
|
@ -134,7 +134,9 @@ const useInpaintingCanvasHotkeys = () => {
|
||||
useHotkeys(
|
||||
['space'],
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.repeat) return;
|
||||
if (e.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasStage?.container().focus();
|
||||
|
||||
|
@ -38,7 +38,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
|
||||
return useCallback(
|
||||
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
if (!stageRef.current) return;
|
||||
if (!stageRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
stageRef.current.container().focus();
|
||||
|
||||
@ -54,7 +56,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
|
||||
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||
|
||||
if (!scaledCursorPosition) return;
|
||||
if (!scaledCursorPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.evt.preventDefault();
|
||||
|
||||
|
@ -41,11 +41,15 @@ const useCanvasMouseMove = (
|
||||
const { updateColorUnderCursor } = useColorPicker();
|
||||
|
||||
return useCallback(() => {
|
||||
if (!stageRef.current) return;
|
||||
if (!stageRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||
|
||||
if (!scaledCursorPosition) return;
|
||||
if (!scaledCursorPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setCursorPosition(scaledCursorPosition));
|
||||
|
||||
@ -56,7 +60,9 @@ const useCanvasMouseMove = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDrawing || tool === 'move' || isStaging) return;
|
||||
if (!isDrawing || tool === 'move' || isStaging) {
|
||||
return;
|
||||
}
|
||||
|
||||
didMouseMoveRef.current = true;
|
||||
dispatch(
|
||||
|
@ -47,7 +47,9 @@ const useCanvasMouseUp = (
|
||||
if (!didMouseMoveRef.current && isDrawing && stageRef.current) {
|
||||
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||
|
||||
if (!scaledCursorPosition) return;
|
||||
if (!scaledCursorPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the current line.
|
||||
|
@ -35,13 +35,17 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
return useCallback(
|
||||
(e: KonvaEventObject<WheelEvent>) => {
|
||||
// stop default scrolling
|
||||
if (!stageRef.current || isMoveStageKeyHeld) return;
|
||||
if (!stageRef.current || isMoveStageKeyHeld) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.evt.preventDefault();
|
||||
|
||||
const cursorPos = stageRef.current.getPointerPosition();
|
||||
|
||||
if (!cursorPos) return;
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mousePointTo = {
|
||||
x: (cursorPos.x - stageRef.current.x()) / stageScale,
|
||||
|
@ -16,11 +16,15 @@ const useColorPicker = () => {
|
||||
|
||||
return {
|
||||
updateColorUnderCursor: () => {
|
||||
if (!stage || !canvasBaseLayer) return;
|
||||
if (!stage || !canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = stage.getPointerPosition();
|
||||
|
||||
if (!position) return;
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pixelRatio = Konva.pixelRatio;
|
||||
|
||||
|
@ -3,8 +3,4 @@ import { CanvasState } from './canvasTypes';
|
||||
/**
|
||||
* Canvas slice persist denylist
|
||||
*/
|
||||
export const canvasPersistDenylist: (keyof CanvasState)[] = [
|
||||
'cursorPosition',
|
||||
'isCanvasInitialized',
|
||||
'doesCanvasNeedScaling',
|
||||
];
|
||||
export const canvasPersistDenylist: (keyof CanvasState)[] = ['cursorPosition'];
|
||||
|
@ -5,10 +5,6 @@ import {
|
||||
roundToMultiple,
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import { setAspectRatio } from 'features/parameters/store/generationSlice';
|
||||
import {
|
||||
setActiveTab,
|
||||
setShouldUseCanvasBetaLayout,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { clamp, cloneDeep } from 'lodash-es';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
@ -50,12 +46,9 @@ export const initialCanvasState: CanvasState = {
|
||||
boundingBoxScaleMethod: 'none',
|
||||
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||
brushSize: 50,
|
||||
canvasContainerDimensions: { width: 0, height: 0 },
|
||||
colorPickerColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||
cursorPosition: null,
|
||||
doesCanvasNeedScaling: false,
|
||||
futureLayerStates: [],
|
||||
isCanvasInitialized: false,
|
||||
isDrawing: false,
|
||||
isMaskEnabled: true,
|
||||
isMouseOverBoundingBox: false,
|
||||
@ -208,7 +201,6 @@ export const canvasSlice = createSlice({
|
||||
};
|
||||
state.futureLayerStates = [];
|
||||
|
||||
state.isCanvasInitialized = false;
|
||||
const newScale = calculateScale(
|
||||
stageDimensions.width,
|
||||
stageDimensions.height,
|
||||
@ -228,7 +220,6 @@ export const canvasSlice = createSlice({
|
||||
);
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = newCoordinates;
|
||||
state.doesCanvasNeedScaling = true;
|
||||
},
|
||||
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||
const newDimensions = roundDimensionsTo64(action.payload);
|
||||
@ -258,9 +249,6 @@ export const canvasSlice = createSlice({
|
||||
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
|
||||
state.boundingBoxPreviewFill = action.payload;
|
||||
},
|
||||
setDoesCanvasNeedScaling: (state, action: PayloadAction<boolean>) => {
|
||||
state.doesCanvasNeedScaling = action.payload;
|
||||
},
|
||||
setStageScale: (state, action: PayloadAction<number>) => {
|
||||
state.stageScale = action.payload;
|
||||
},
|
||||
@ -397,7 +385,9 @@ export const canvasSlice = createSlice({
|
||||
const { tool, layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
|
||||
state;
|
||||
|
||||
if (tool === 'move' || tool === 'colorPicker') return;
|
||||
if (tool === 'move' || tool === 'colorPicker') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStrokeWidth = brushSize / 2;
|
||||
|
||||
@ -434,14 +424,18 @@ export const canvasSlice = createSlice({
|
||||
addPointToCurrentLine: (state, action: PayloadAction<number[]>) => {
|
||||
const lastLine = state.layerState.objects.findLast(isCanvasAnyLine);
|
||||
|
||||
if (!lastLine) return;
|
||||
if (!lastLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastLine.points.push(...action.payload);
|
||||
},
|
||||
undo: (state) => {
|
||||
const targetState = state.pastLayerStates.pop();
|
||||
|
||||
if (!targetState) return;
|
||||
if (!targetState) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.futureLayerStates.unshift(cloneDeep(state.layerState));
|
||||
|
||||
@ -454,7 +448,9 @@ export const canvasSlice = createSlice({
|
||||
redo: (state) => {
|
||||
const targetState = state.futureLayerStates.shift();
|
||||
|
||||
if (!targetState) return;
|
||||
if (!targetState) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
|
||||
@ -485,97 +481,14 @@ export const canvasSlice = createSlice({
|
||||
state.layerState = initialLayerState;
|
||||
state.futureLayerStates = [];
|
||||
},
|
||||
setCanvasContainerDimensions: (
|
||||
canvasResized: (
|
||||
state,
|
||||
action: PayloadAction<Dimensions>
|
||||
action: PayloadAction<{ width: number; height: number }>
|
||||
) => {
|
||||
state.canvasContainerDimensions = action.payload;
|
||||
},
|
||||
resizeAndScaleCanvas: (state) => {
|
||||
const { width: containerWidth, height: containerHeight } =
|
||||
state.canvasContainerDimensions;
|
||||
|
||||
const initialCanvasImage =
|
||||
state.layerState.objects.find(isCanvasBaseImage);
|
||||
|
||||
const { width, height } = action.payload;
|
||||
const newStageDimensions = {
|
||||
width: Math.floor(containerWidth),
|
||||
height: Math.floor(containerHeight),
|
||||
};
|
||||
|
||||
if (!initialCanvasImage) {
|
||||
const newScale = calculateScale(
|
||||
newStageDimensions.width,
|
||||
newStageDimensions.height,
|
||||
512,
|
||||
512,
|
||||
STAGE_PADDING_PERCENTAGE
|
||||
);
|
||||
|
||||
const newCoordinates = calculateCoordinates(
|
||||
newStageDimensions.width,
|
||||
newStageDimensions.height,
|
||||
0,
|
||||
0,
|
||||
512,
|
||||
512,
|
||||
newScale
|
||||
);
|
||||
|
||||
const newBoundingBoxDimensions = { width: 512, height: 512 };
|
||||
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = newCoordinates;
|
||||
state.stageDimensions = newStageDimensions;
|
||||
state.boundingBoxCoordinates = { x: 0, y: 0 };
|
||||
state.boundingBoxDimensions = newBoundingBoxDimensions;
|
||||
|
||||
if (state.boundingBoxScaleMethod === 'auto') {
|
||||
const scaledDimensions = getScaledBoundingBoxDimensions(
|
||||
newBoundingBoxDimensions
|
||||
);
|
||||
state.scaledBoundingBoxDimensions = scaledDimensions;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { width: imageWidth, height: imageHeight } = initialCanvasImage;
|
||||
|
||||
const padding = 0.95;
|
||||
|
||||
const newScale = calculateScale(
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
padding
|
||||
);
|
||||
|
||||
const newCoordinates = calculateCoordinates(
|
||||
newStageDimensions.width,
|
||||
newStageDimensions.height,
|
||||
0,
|
||||
0,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
newScale
|
||||
);
|
||||
|
||||
state.minimumStageScale = newScale;
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = floorCoordinates(newCoordinates);
|
||||
state.stageDimensions = newStageDimensions;
|
||||
|
||||
state.isCanvasInitialized = true;
|
||||
},
|
||||
resizeCanvas: (state) => {
|
||||
const { width: containerWidth, height: containerHeight } =
|
||||
state.canvasContainerDimensions;
|
||||
|
||||
const newStageDimensions = {
|
||||
width: Math.floor(containerWidth),
|
||||
height: Math.floor(containerHeight),
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height),
|
||||
};
|
||||
|
||||
state.stageDimensions = newStageDimensions;
|
||||
@ -868,14 +781,6 @@ export const canvasSlice = createSlice({
|
||||
state.layerState.stagingArea = initialLayerState.stagingArea;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(setShouldUseCanvasBetaLayout, (state) => {
|
||||
state.doesCanvasNeedScaling = true;
|
||||
});
|
||||
|
||||
builder.addCase(setActiveTab, (state) => {
|
||||
state.doesCanvasNeedScaling = true;
|
||||
});
|
||||
builder.addCase(setAspectRatio, (state, action) => {
|
||||
const ratio = action.payload;
|
||||
if (ratio) {
|
||||
@ -907,8 +812,6 @@ export const {
|
||||
resetCanvas,
|
||||
resetCanvasInteractionState,
|
||||
resetCanvasView,
|
||||
resizeAndScaleCanvas,
|
||||
resizeCanvas,
|
||||
setBoundingBoxCoordinates,
|
||||
setBoundingBoxDimensions,
|
||||
setBoundingBoxPreviewFill,
|
||||
@ -916,10 +819,8 @@ export const {
|
||||
flipBoundingBoxAxes,
|
||||
setBrushColor,
|
||||
setBrushSize,
|
||||
setCanvasContainerDimensions,
|
||||
setColorPickerColor,
|
||||
setCursorPosition,
|
||||
setDoesCanvasNeedScaling,
|
||||
setInitialCanvasImage,
|
||||
setIsDrawing,
|
||||
setIsMaskEnabled,
|
||||
@ -958,6 +859,7 @@ export const {
|
||||
stagingAreaInitialized,
|
||||
canvasSessionIdChanged,
|
||||
setShouldAntialias,
|
||||
canvasResized,
|
||||
} = canvasSlice.actions;
|
||||
|
||||
export default canvasSlice.reducer;
|
||||
|
@ -126,12 +126,9 @@ export interface CanvasState {
|
||||
boundingBoxScaleMethod: BoundingBoxScale;
|
||||
brushColor: RgbaColor;
|
||||
brushSize: number;
|
||||
canvasContainerDimensions: Dimensions;
|
||||
colorPickerColor: RgbaColor;
|
||||
cursorPosition: Vector2d | null;
|
||||
doesCanvasNeedScaling: boolean;
|
||||
futureLayerStates: CanvasLayerState[];
|
||||
isCanvasInitialized: boolean;
|
||||
isDrawing: boolean;
|
||||
isMaskEnabled: boolean;
|
||||
isMouseOverBoundingBox: boolean;
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { setDoesCanvasNeedScaling } from '../canvasSlice';
|
||||
|
||||
const debouncedCanvasScale = debounce((dispatch: AppDispatch) => {
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
}, 300);
|
||||
|
||||
export const requestCanvasRescale =
|
||||
() => (dispatch: AppDispatch, getState: AppGetState) => {
|
||||
const activeTabName = activeTabNameSelector(getState());
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
debouncedCanvasScale(dispatch);
|
||||
}
|
||||
};
|
@ -5,7 +5,9 @@ const getScaledCursorPosition = (stage: Stage) => {
|
||||
|
||||
const stageTransform = stage.getAbsoluteTransform().copy();
|
||||
|
||||
if (!pointerPosition || !stageTransform) return;
|
||||
if (!pointerPosition || !stageTransform) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scaledCursorPosition = stageTransform.invert().point(pointerPosition);
|
||||
|
||||
|
@ -80,19 +80,19 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 3,
|
||||
p: 3,
|
||||
p: 2,
|
||||
borderRadius: 'base',
|
||||
position: 'relative',
|
||||
bg: 'base.200',
|
||||
bg: 'base.250',
|
||||
_dark: {
|
||||
bg: 'base.850',
|
||||
bg: 'base.750',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<IAISwitch
|
||||
tooltip={'Toggle this ControlNet'}
|
||||
aria-label={'Toggle this ControlNet'}
|
||||
tooltip="Toggle this ControlNet"
|
||||
aria-label="Toggle this ControlNet"
|
||||
isChecked={isEnabled}
|
||||
onChange={handleToggleIsEnabled}
|
||||
/>
|
||||
@ -194,7 +194,7 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
>
|
||||
<ControlNetImagePreview controlNet={controlNet} height={28} />
|
||||
<ControlNetImagePreview controlNet={controlNet} isSmall />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
@ -207,7 +207,7 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
|
||||
{isExpanded && (
|
||||
<>
|
||||
<ControlNetImagePreview controlNet={controlNet} height="392px" />
|
||||
<ControlNetImagePreview controlNet={controlNet} />
|
||||
<ParamControlNetShouldAutoConfig controlNet={controlNet} />
|
||||
<ControlNetProcessorComponent controlNet={controlNet} />
|
||||
</>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Box, Flex, Spinner, SystemStyleObject } from '@chakra-ui/react';
|
||||
import { Box, Flex, Spinner } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import {
|
||||
TypesafeDraggableData,
|
||||
TypesafeDroppableData,
|
||||
} from 'features/dnd/types';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import {
|
||||
TypesafeDraggableData,
|
||||
TypesafeDroppableData,
|
||||
} from 'features/dnd/types';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { FaUndo } from 'react-icons/fa';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
@ -21,7 +21,7 @@ import {
|
||||
|
||||
type Props = {
|
||||
controlNet: ControlNetConfig;
|
||||
height: SystemStyleObject['h'];
|
||||
isSmall?: boolean;
|
||||
};
|
||||
|
||||
const selector = createSelector(
|
||||
@ -36,15 +36,14 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const ControlNetImagePreview = (props: Props) => {
|
||||
const { height } = props;
|
||||
const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
|
||||
const {
|
||||
controlImage: controlImageName,
|
||||
processedControlImage: processedControlImageName,
|
||||
processorType,
|
||||
isEnabled,
|
||||
controlNetId,
|
||||
} = props.controlNet;
|
||||
} = controlNet;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -109,7 +108,7 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
sx={{
|
||||
position: 'relative',
|
||||
w: 'full',
|
||||
h: height,
|
||||
h: isSmall ? 28 : 366, // magic no touch
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: isEnabled ? 'auto' : 'none',
|
||||
|
@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { isControlNetEnabledToggled } from 'features/controlNet/store/controlNetSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
@ -36,4 +36,4 @@ const ParamControlNetFeatureToggle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamControlNetFeatureToggle;
|
||||
export default memo(ParamControlNetFeatureToggle);
|
||||
|
@ -23,7 +23,7 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
|
||||
return (
|
||||
<IAISlider
|
||||
isDisabled={!isEnabled}
|
||||
label={'Weight'}
|
||||
label="Weight"
|
||||
value={weight}
|
||||
onChange={handleWeightChanged}
|
||||
min={0}
|
||||
|
@ -8,6 +8,7 @@ import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial
|
||||
import ParamDynamicPromptsToggle from './ParamDynamicPromptsEnabled';
|
||||
import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts';
|
||||
import { useFeatureStatus } from '../../system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
@ -40,4 +41,4 @@ const ParamDynamicPromptsCollapse = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamDynamicPromptsCollapse;
|
||||
export default memo(ParamDynamicPromptsCollapse);
|
||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { combinatorialToggled } from '../store/dynamicPromptsSlice';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -34,4 +34,4 @@ const ParamDynamicPromptsCombinatorial = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamDynamicPromptsCombinatorial;
|
||||
export default memo(ParamDynamicPromptsCombinatorial);
|
||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { isEnabledToggled } from '../store/dynamicPromptsSlice';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -33,4 +33,4 @@ const ParamDynamicPromptsToggle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamDynamicPromptsToggle;
|
||||
export default memo(ParamDynamicPromptsToggle);
|
||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import {
|
||||
maxPromptsChanged,
|
||||
maxPromptsReset,
|
||||
@ -60,4 +60,4 @@ const ParamDynamicPromptsMaxPrompts = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamDynamicPromptsMaxPrompts;
|
||||
export default memo(ParamDynamicPromptsMaxPrompts);
|
||||
|
@ -13,7 +13,7 @@ import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSe
|
||||
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
||||
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { PropsWithChildren, useCallback, useMemo, useRef } from 'react';
|
||||
import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models';
|
||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
||||
|
||||
@ -118,7 +118,7 @@ const ParamEmbeddingPopover = (props: Props) => {
|
||||
<IAIMantineSearchableSelect
|
||||
inputRef={inputRef}
|
||||
autoFocus
|
||||
placeholder={'Add Embedding'}
|
||||
placeholder="Add Embedding"
|
||||
value={null}
|
||||
data={data}
|
||||
nothingFound="No matching Embeddings"
|
||||
@ -140,4 +140,4 @@ const ParamEmbeddingPopover = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamEmbeddingPopover;
|
||||
export default memo(ParamEmbeddingPopover);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Badge, Flex } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const AutoAddIcon = () => {
|
||||
return (
|
||||
@ -20,4 +21,4 @@ const AutoAddIcon = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoAddIcon;
|
||||
export default memo(AutoAddIcon);
|
||||
|
@ -6,7 +6,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -66,7 +66,7 @@ const BoardAutoAddSelect = () => {
|
||||
label="Auto-Add Board"
|
||||
inputRef={inputRef}
|
||||
autoFocus
|
||||
placeholder={'Select a Board'}
|
||||
placeholder="Select a Board"
|
||||
value={autoAddBoardId}
|
||||
data={boards}
|
||||
nothingFound="No matching Boards"
|
||||
@ -81,4 +81,4 @@ const BoardAutoAddSelect = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardAutoAddSelect;
|
||||
export default memo(BoardAutoAddSelect);
|
||||
|
@ -2,8 +2,12 @@ import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import {
|
||||
IAIContextMenu,
|
||||
IAIContextMenuProps,
|
||||
} from 'common/components/IAIContextMenu';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { BoardId } from 'features/gallery/store/types';
|
||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
@ -11,80 +15,80 @@ import { BoardDTO } from 'services/api/types';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
||||
import { BoardId } from 'features/gallery/store/types';
|
||||
|
||||
type Props = {
|
||||
board?: BoardDTO;
|
||||
board_id: BoardId;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
children: IAIContextMenuProps<HTMLDivElement>['children'];
|
||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||
};
|
||||
|
||||
const BoardContextMenu = memo(
|
||||
({ board, board_id, setBoardToDelete, children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const BoardContextMenu = ({
|
||||
board,
|
||||
board_id,
|
||||
setBoardToDelete,
|
||||
children,
|
||||
}: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(stateSelector, ({ gallery, system }) => {
|
||||
const isAutoAdd = gallery.autoAddBoardId === board_id;
|
||||
const isProcessing = system.isProcessing;
|
||||
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
||||
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
|
||||
}),
|
||||
[board_id]
|
||||
);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(stateSelector, ({ gallery, system }) => {
|
||||
const isAutoAdd = gallery.autoAddBoardId === board_id;
|
||||
const isProcessing = system.isProcessing;
|
||||
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
||||
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
|
||||
}),
|
||||
[board_id]
|
||||
);
|
||||
|
||||
const { isAutoAdd, isProcessing, autoAssignBoardOnClick } =
|
||||
useAppSelector(selector);
|
||||
const boardName = useBoardName(board_id);
|
||||
const { isAutoAdd, isProcessing, autoAssignBoardOnClick } =
|
||||
useAppSelector(selector);
|
||||
const boardName = useBoardName(board_id);
|
||||
|
||||
const handleSetAutoAdd = useCallback(() => {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
const handleSetAutoAdd = useCallback(() => {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
menuButtonProps={{
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
<MenuGroup title={boardName}>
|
||||
<MenuItem
|
||||
icon={<FaPlus />}
|
||||
isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick}
|
||||
onClick={handleSetAutoAdd}
|
||||
>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
{!board && <NoBoardContextMenuItems />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
)}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<IAIContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
menuButtonProps={{
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
<MenuGroup title={boardName}>
|
||||
<MenuItem
|
||||
icon={<FaPlus />}
|
||||
isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick}
|
||||
onClick={handleSetAutoAdd}
|
||||
>
|
||||
Auto-add to this Board
|
||||
</MenuItem>
|
||||
{!board && <NoBoardContextMenuItems />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
)}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</IAIContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
BoardContextMenu.displayName = 'HoverableBoard';
|
||||
|
||||
export default BoardContextMenu;
|
||||
export default memo(BoardContextMenu);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
|
||||
|
||||
@ -24,4 +24,4 @@ const AddBoardButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AddBoardButton;
|
||||
export default memo(AddBoardButton);
|
||||
|
@ -41,7 +41,7 @@ const BoardsList = (props: Props) => {
|
||||
<>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Flex
|
||||
layerStyle={'first'}
|
||||
layerStyle="first"
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
|
@ -39,187 +39,188 @@ interface GalleryBoardProps {
|
||||
setBoardToDelete: (board?: BoardDTO) => void;
|
||||
}
|
||||
|
||||
const GalleryBoard = memo(
|
||||
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ gallery, system }) => {
|
||||
const isSelectedForAutoAdd =
|
||||
board.board_id === gallery.autoAddBoardId;
|
||||
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
||||
const isProcessing = system.isProcessing;
|
||||
const GalleryBoard = ({
|
||||
board,
|
||||
isSelected,
|
||||
setBoardToDelete,
|
||||
}: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ gallery, system }) => {
|
||||
const isSelectedForAutoAdd =
|
||||
board.board_id === gallery.autoAddBoardId;
|
||||
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
||||
const isProcessing = system.isProcessing;
|
||||
|
||||
return {
|
||||
isSelectedForAutoAdd,
|
||||
autoAssignBoardOnClick,
|
||||
isProcessing,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[board.board_id]
|
||||
);
|
||||
return {
|
||||
isSelectedForAutoAdd,
|
||||
autoAssignBoardOnClick,
|
||||
isProcessing,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } =
|
||||
useAppSelector(selector);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
const handleMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } =
|
||||
useAppSelector(selector);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
const handleMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
|
||||
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
|
||||
const tooltip = useMemo(() => {
|
||||
if (!imagesTotal || !assetsTotal) {
|
||||
return undefined;
|
||||
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
|
||||
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
|
||||
const tooltip = useMemo(() => {
|
||||
if (!imagesTotal || !assetsTotal) {
|
||||
return undefined;
|
||||
}
|
||||
return `${imagesTotal} image${
|
||||
imagesTotal > 1 ? 's' : ''
|
||||
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`;
|
||||
}, [assetsTotal, imagesTotal]);
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||
board.cover_image_name ?? skipToken
|
||||
);
|
||||
|
||||
const { board_name, board_id } = board;
|
||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected(board_id));
|
||||
if (autoAssignBoardOnClick && !isProcessing) {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
}
|
||||
}, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]);
|
||||
|
||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||
useUpdateBoardMutation();
|
||||
|
||||
const droppableData: AddToBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: board_id,
|
||||
actionType: 'ADD_TO_BOARD',
|
||||
context: { boardId: board_id },
|
||||
}),
|
||||
[board_id]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (newBoardName: string) => {
|
||||
// empty strings are not allowed
|
||||
if (!newBoardName.trim()) {
|
||||
setLocalBoardName(board_name);
|
||||
return;
|
||||
}
|
||||
return `${imagesTotal} image${
|
||||
imagesTotal > 1 ? 's' : ''
|
||||
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`;
|
||||
}, [assetsTotal, imagesTotal]);
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||
board.cover_image_name ?? skipToken
|
||||
);
|
||||
|
||||
const { board_name, board_id } = board;
|
||||
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected(board_id));
|
||||
if (autoAssignBoardOnClick && !isProcessing) {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
// don't updated the board name if it hasn't changed
|
||||
if (newBoardName === board_name) {
|
||||
return;
|
||||
}
|
||||
}, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]);
|
||||
|
||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||
useUpdateBoardMutation();
|
||||
try {
|
||||
const { board_name } = await updateBoard({
|
||||
board_id,
|
||||
changes: { board_name: newBoardName },
|
||||
}).unwrap();
|
||||
|
||||
const droppableData: AddToBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: board_id,
|
||||
actionType: 'ADD_TO_BOARD',
|
||||
context: { boardId: board_id },
|
||||
}),
|
||||
[board_id]
|
||||
);
|
||||
// update local state
|
||||
setLocalBoardName(board_name);
|
||||
} catch {
|
||||
// revert on error
|
||||
setLocalBoardName(board_name);
|
||||
}
|
||||
},
|
||||
[board_id, board_name, updateBoard]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (newBoardName: string) => {
|
||||
// empty strings are not allowed
|
||||
if (!newBoardName.trim()) {
|
||||
setLocalBoardName(board_name);
|
||||
return;
|
||||
}
|
||||
const handleChange = useCallback((newBoardName: string) => {
|
||||
setLocalBoardName(newBoardName);
|
||||
}, []);
|
||||
|
||||
// don't updated the board name if it hasn't changed
|
||||
if (newBoardName === board_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { board_name } = await updateBoard({
|
||||
board_id,
|
||||
changes: { board_name: newBoardName },
|
||||
}).unwrap();
|
||||
|
||||
// update local state
|
||||
setLocalBoardName(board_name);
|
||||
} catch {
|
||||
// revert on error
|
||||
setLocalBoardName(board_name);
|
||||
}
|
||||
},
|
||||
[board_id, board_name, updateBoard]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((newBoardName: string) => {
|
||||
setLocalBoardName(newBoardName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
<BoardContextMenu
|
||||
board={board}
|
||||
board_id={board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
>
|
||||
<BoardContextMenu
|
||||
board={board}
|
||||
board_id={board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
>
|
||||
{(ref) => (
|
||||
<Tooltip label={tooltip} openDelay={1000} hasArrow>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
{(ref) => (
|
||||
<Tooltip label={tooltip} openDelay={1000} hasArrow>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
bg: 'base.200',
|
||||
_dark: {
|
||||
bg: 'base.800',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
boxSize={12}
|
||||
as={FaUser}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
borderBottomRadius: 'lg',
|
||||
mt: -6,
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
_dark: {
|
||||
color: 'base.500',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
boxSize={12}
|
||||
as={FaUser}
|
||||
sx={{
|
||||
mt: -6,
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
_dark: {
|
||||
color: 'base.500',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{/* <Flex
|
||||
</Flex>
|
||||
)}
|
||||
{/* <Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
@ -231,80 +232,77 @@ const GalleryBoard = memo(
|
||||
{totalImages}/{totalAssets}
|
||||
</Badge>
|
||||
</Flex> */}
|
||||
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||
<SelectionOverlay
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
p: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
w: 'full',
|
||||
maxW: 'full',
|
||||
borderBottomRadius: 'base',
|
||||
bg: isSelected ? 'accent.400' : 'base.500',
|
||||
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||
<SelectionOverlay
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
p: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
w: 'full',
|
||||
maxW: 'full',
|
||||
borderBottomRadius: 'base',
|
||||
bg: isSelected ? 'accent.400' : 'base.500',
|
||||
color: isSelected ? 'base.50' : 'base.100',
|
||||
_dark: {
|
||||
bg: isSelected ? 'accent.500' : 'base.600',
|
||||
color: isSelected ? 'base.50' : 'base.100',
|
||||
_dark: {
|
||||
bg: isSelected ? 'accent.500' : 'base.600',
|
||||
color: isSelected ? 'base.50' : 'base.100',
|
||||
},
|
||||
lineHeight: 'short',
|
||||
fontSize: 'xs',
|
||||
},
|
||||
lineHeight: 'short',
|
||||
fontSize: 'xs',
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
value={localBoardName}
|
||||
isDisabled={isUpdateBoardLoading}
|
||||
submitOnBlur={true}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
sx={{
|
||||
w: 'full',
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
value={localBoardName}
|
||||
isDisabled={isUpdateBoardLoading}
|
||||
submitOnBlur={true}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
<EditablePreview
|
||||
sx={{
|
||||
w: 'full',
|
||||
p: 0,
|
||||
fontWeight: isSelected ? 700 : 500,
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
<EditablePreview
|
||||
sx={{
|
||||
noOfLines={1}
|
||||
/>
|
||||
<EditableInput
|
||||
sx={{
|
||||
p: 0,
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
fontWeight: isSelected ? 700 : 500,
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
noOfLines={1}
|
||||
/>
|
||||
<EditableInput
|
||||
sx={{
|
||||
p: 0,
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
textAlign: 'center',
|
||||
// get rid of the edit border
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Editable>
|
||||
</Flex>
|
||||
|
||||
<IAIDroppable
|
||||
data={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
/>
|
||||
// get rid of the edit border
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Editable>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GalleryBoard.displayName = 'HoverableBoard';
|
||||
<IAIDroppable
|
||||
data={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryBoard;
|
||||
export default memo(GalleryBoard);
|
||||
|
@ -3,7 +3,7 @@ import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { BoardId } from 'features/gallery/store/types';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, memo } from 'react';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
type GenericBoardProps = {
|
||||
@ -105,4 +105,4 @@ const GenericBoard = (props: GenericBoardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericBoard;
|
||||
export default memo(GenericBoard);
|
||||
|
@ -156,4 +156,4 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
|
||||
NoBoardBoard.displayName = 'HoverableBoard';
|
||||
|
||||
export default NoBoardBoard;
|
||||
export default memo(NoBoardBoard);
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
setShouldShowImageDetails,
|
||||
setShouldShowProgressInViewer,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@ -323,4 +323,4 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageButtons;
|
||||
export default memo(CurrentImageButtons);
|
||||
|
@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import { memo } from 'react';
|
||||
|
||||
const CurrentImageDisplay = () => {
|
||||
return (
|
||||
@ -22,4 +23,4 @@ const CurrentImageDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageDisplay;
|
||||
export default memo(CurrentImageDisplay);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
const CurrentImageHidden = () => {
|
||||
@ -18,4 +19,4 @@ const CurrentImageHidden = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageHidden;
|
||||
export default memo(CurrentImageHidden);
|
||||
|
@ -1,119 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
||||
import { memo } from 'react';
|
||||
import ImageGalleryContent from './ImageGalleryContent';
|
||||
|
||||
const selector = createSelector(
|
||||
[activeTabNameSelector, uiSelector, gallerySelector, isStagingSelector],
|
||||
(activeTabName, ui, gallery, isStaging) => {
|
||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
||||
const { galleryImageMinimumWidth } = gallery;
|
||||
|
||||
return {
|
||||
activeTabName,
|
||||
isStaging,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
isResizable: activeTabName !== 'unifiedCanvas',
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const GalleryDrawer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
// activeTabName,
|
||||
// isStaging,
|
||||
// isResizable,
|
||||
} = useAppSelector(selector);
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinGallery,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
const IMAGE_SIZE_STEP = 32;
|
||||
|
||||
useHotkeys(
|
||||
'shift+up',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth < 256) {
|
||||
const newMinWidth = clamp(
|
||||
galleryImageMinimumWidth + IMAGE_SIZE_STEP,
|
||||
32,
|
||||
256
|
||||
);
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+down',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth > 32) {
|
||||
const newMinWidth = clamp(
|
||||
galleryImageMinimumWidth - IMAGE_SIZE_STEP,
|
||||
32,
|
||||
256
|
||||
);
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
if (shouldPinGallery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizableDrawer
|
||||
direction="right"
|
||||
isResizable={true}
|
||||
isOpen={shouldShowGallery}
|
||||
onClose={handleCloseGallery}
|
||||
minWidth={400}
|
||||
>
|
||||
<ImageGalleryContent />
|
||||
</ResizableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GalleryDrawer);
|
@ -1,44 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { shouldPinGallery } = state.ui;
|
||||
|
||||
return {
|
||||
shouldPinGallery,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const GalleryPinButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { shouldPinGallery } = useAppSelector(selector);
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(togglePinGalleryPanel());
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
return (
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('gallery.pinGallery')}
|
||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryPinButton;
|
@ -12,7 +12,7 @@ import {
|
||||
setGalleryImageMinimumWidth,
|
||||
shouldAutoSwitchChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaWrench } from 'react-icons/fa';
|
||||
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
||||
@ -101,4 +101,4 @@ const GallerySettingsPopover = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GallerySettingsPopover;
|
||||
export default memo(GallerySettingsPopover);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
isModalOpenChanged,
|
||||
} from 'features/changeBoardModal/store/slice';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaFolder, FaTrash } from 'react-icons/fa';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
import {
|
||||
@ -74,4 +74,4 @@ const MultipleSelectionMenuItems = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MultipleSelectionMenuItems;
|
||||
export default memo(MultipleSelectionMenuItems);
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { Flex, MenuItem, Text } from '@chakra-ui/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
resizeAndScaleCanvas,
|
||||
setInitialCanvasImage,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
imagesToChangeSelected,
|
||||
isModalOpenChanged,
|
||||
@ -29,6 +26,7 @@ import {
|
||||
FaShare,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
import {
|
||||
useGetImageMetadataQuery,
|
||||
useStarImagesMutation,
|
||||
@ -37,7 +35,6 @@ import {
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
@ -110,7 +107,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setInitialCanvasImage(imageDTO));
|
||||
dispatch(resizeAndScaleCanvas());
|
||||
dispatch(setActiveTab('unifiedCanvas'));
|
||||
|
||||
toaster({
|
||||
@ -136,11 +132,15 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||
|
||||
const handleStarImage = useCallback(() => {
|
||||
if (imageDTO) starImages({ imageDTOs: [imageDTO] });
|
||||
if (imageDTO) {
|
||||
starImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [starImages, imageDTO]);
|
||||
|
||||
const handleUnstarImage = useCallback(() => {
|
||||
if (imageDTO) unstarImages({ imageDTOs: [imageDTO] });
|
||||
if (imageDTO) {
|
||||
unstarImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [unstarImages, imageDTO]);
|
||||
|
||||
return (
|
||||
@ -228,6 +228,18 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
>
|
||||
{t('gallery.deleteImage')}
|
||||
</MenuItem>
|
||||
{metadata?.created_by && (
|
||||
<Flex
|
||||
sx={{
|
||||
padding: '5px 10px',
|
||||
marginTop: '5px',
|
||||
}}
|
||||
>
|
||||
<Text fontSize="xs" fontWeight="bold">
|
||||
Created by {metadata?.created_by}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type ImageFallbackSpinnerProps = SpinnerProps;
|
||||
|
||||
@ -23,4 +24,4 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageFallbackSpinner;
|
||||
export default memo(ImageFallbackSpinner);
|
||||
|
@ -18,7 +18,6 @@ import { FaImages, FaServer } from 'react-icons/fa';
|
||||
import { galleryViewChanged } from '../store/gallerySlice';
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import GalleryPinButton from './GalleryPinButton';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
|
||||
@ -75,7 +74,6 @@ const ImageGalleryContent = () => {
|
||||
onToggle={onToggleBoardList}
|
||||
/>
|
||||
<GallerySettingsPopover />
|
||||
<GalleryPinButton />
|
||||
</Flex>
|
||||
<Box>
|
||||
<BoardsList isOpen={isBoardListOpen} />
|
||||
|
@ -88,8 +88,12 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
}, []);
|
||||
|
||||
const starIcon = useMemo(() => {
|
||||
if (imageDTO?.starred) return <MdStar size="20" />;
|
||||
if (!imageDTO?.starred && isHovered) return <MdStarBorder size="20" />;
|
||||
if (imageDTO?.starred) {
|
||||
return <MdStar size="20" />;
|
||||
}
|
||||
if (!imageDTO?.starred && isHovered) {
|
||||
return <MdStarBorder size="20" />;
|
||||
}
|
||||
}, [imageDTO?.starred, isHovered]);
|
||||
|
||||
if (!imageDTO) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, FlexProps, forwardRef } from '@chakra-ui/react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
@ -8,4 +8,4 @@ const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
</Box>
|
||||
));
|
||||
|
||||
export default ItemContainer;
|
||||
export default memo(ItemContainer);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FlexProps, Grid, forwardRef } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
@ -23,4 +23,4 @@ const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default ListContainer;
|
||||
export default memo(ListContainer);
|
||||
|
@ -1,34 +1,37 @@
|
||||
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { isString } from 'lodash-es';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaCopy, FaSave } from 'react-icons/fa';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
jsonObject: object;
|
||||
data: object | string;
|
||||
fileName?: string;
|
||||
withDownload?: boolean;
|
||||
withCopy?: boolean;
|
||||
};
|
||||
|
||||
const ImageMetadataJSON = (props: Props) => {
|
||||
const { label, jsonObject, fileName } = props;
|
||||
const jsonString = useMemo(
|
||||
() => JSON.stringify(jsonObject, null, 2),
|
||||
[jsonObject]
|
||||
const DataViewer = (props: Props) => {
|
||||
const { label, data, fileName, withDownload = true, withCopy = true } = props;
|
||||
const dataString = useMemo(
|
||||
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
|
||||
[data]
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(jsonString);
|
||||
}, [jsonString]);
|
||||
navigator.clipboard.writeText(dataString);
|
||||
}, [dataString]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const blob = new Blob([jsonString]);
|
||||
const blob = new Blob([dataString]);
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${fileName || label}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}, [jsonString, label, fileName]);
|
||||
}, [dataString, label, fileName]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -65,31 +68,35 @@ const ImageMetadataJSON = (props: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<pre>{jsonString}</pre>
|
||||
<pre>{dataString}</pre>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Box>
|
||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
||||
<Tooltip label={`Save ${label} JSON`}>
|
||||
<IconButton
|
||||
aria-label={`Save ${label} JSON`}
|
||||
icon={<FaSave />}
|
||||
variant="ghost"
|
||||
opacity={0.7}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={`Copy ${label} JSON`}>
|
||||
<IconButton
|
||||
aria-label={`Copy ${label} JSON`}
|
||||
icon={<FaCopy />}
|
||||
variant="ghost"
|
||||
opacity={0.7}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
{withDownload && (
|
||||
<Tooltip label={`Save ${label} JSON`}>
|
||||
<IconButton
|
||||
aria-label={`Save ${label} JSON`}
|
||||
icon={<FaSave />}
|
||||
variant="ghost"
|
||||
opacity={0.7}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{withCopy && (
|
||||
<Tooltip label={`Copy ${label} JSON`}>
|
||||
<IconButton
|
||||
aria-label={`Copy ${label} JSON`}
|
||||
icon={<FaCopy />}
|
||||
variant="ghost"
|
||||
opacity={0.7}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageMetadataJSON;
|
||||
export default memo(DataViewer);
|
@ -1,5 +1,5 @@
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { UnsafeImageMetadata } from 'services/api/types';
|
||||
import ImageMetadataItem from './ImageMetadataItem';
|
||||
|
||||
@ -69,6 +69,9 @@ const ImageMetadataActions = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{metadata.created_by && (
|
||||
<ImageMetadataItem label="Created By" value={metadata.created_by} />
|
||||
)}
|
||||
{metadata.generation_mode && (
|
||||
<ImageMetadataItem
|
||||
label="Generation Mode"
|
||||
@ -206,4 +209,4 @@ const ImageMetadataActions = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageMetadataActions;
|
||||
export default memo(ImageMetadataActions);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
@ -74,4 +75,4 @@ const ImageMetadataItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageMetadataItem;
|
||||
export default memo(ImageMetadataItem);
|
||||
|
@ -16,7 +16,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import ImageMetadataActions from './ImageMetadataActions';
|
||||
import ImageMetadataJSON from './ImageMetadataJSON';
|
||||
import DataViewer from './DataViewer';
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: ImageDTO;
|
||||
@ -79,21 +79,21 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
{metadata ? (
|
||||
<ImageMetadataJSON jsonObject={metadata} label="Core Metadata" />
|
||||
<DataViewer data={metadata} label="Core Metadata" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No core metadata found" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{image ? (
|
||||
<ImageMetadataJSON jsonObject={image} label="Image Details" />
|
||||
<DataViewer data={image} label="Image Details" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No image details found" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{graph ? (
|
||||
<ImageMetadataJSON jsonObject={graph} label="Graph" />
|
||||
<DataViewer data={graph} label="Graph" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No graph found" />
|
||||
)}
|
||||
|
@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { map } from 'lodash-es';
|
||||
import ParamLora from './ParamLora';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
@ -29,4 +30,4 @@ const ParamLoraList = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamLoraList;
|
||||
export default memo(ParamLoraList);
|
||||
|
@ -9,7 +9,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI
|
||||
import { loraAdded } from 'features/lora/store/loraSlice';
|
||||
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useGetLoRAModelsQuery } from 'services/api/endpoints/models';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -102,4 +102,4 @@ const ParamLoRASelect = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamLoRASelect;
|
||||
export default memo(ParamLoRASelect);
|
||||
|
@ -1,140 +0,0 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import { map } from 'lodash-es';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import { useBuildNodeData } from '../hooks/useBuildNodeData';
|
||||
import { nodeAdded } from '../store/nodesSlice';
|
||||
|
||||
type NodeTemplate = {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
({ nodes }) => {
|
||||
const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => {
|
||||
return {
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
tags: template.tags,
|
||||
};
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Progress Image',
|
||||
value: 'current_image',
|
||||
description: 'Displays the current image in the Node Editor',
|
||||
tags: ['progress'],
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Notes',
|
||||
value: 'notes',
|
||||
description: 'Add notes about your workflow',
|
||||
tags: ['notes'],
|
||||
});
|
||||
|
||||
return { data };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const AddNodeMenu = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { data } = useAppSelector(selector);
|
||||
|
||||
const buildInvocation = useBuildNodeData();
|
||||
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: AnyInvocationType) => {
|
||||
const invocation = buildInvocation(nodeType);
|
||||
|
||||
if (!invocation) {
|
||||
toaster({
|
||||
status: 'error',
|
||||
title: `Unknown Invocation type ${nodeType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(nodeAdded(invocation));
|
||||
},
|
||||
[dispatch, buildInvocation, toaster]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: string | null) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNode(v as AnyInvocationType);
|
||||
},
|
||||
[addNode]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<IAIMantineSearchableSelect
|
||||
selectOnBlur={false}
|
||||
placeholder="Add Node"
|
||||
value={null}
|
||||
data={data}
|
||||
maxDropdownHeight={400}
|
||||
nothingFound="No matching nodes"
|
||||
itemComponent={SelectItem}
|
||||
filter={(value, item: NodeTemplate) =>
|
||||
item.label.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||
item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||
item.description.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||
item.tags.includes(value.toLowerCase().trim())
|
||||
}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
width: '24rem',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ label, description, ...others }: ItemProps, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...others}>
|
||||
<div>
|
||||
<Text fontWeight={600}>{label}</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
sx={{ color: 'base.600', _dark: { color: 'base.500' } }}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectItem.displayName = 'SelectItem';
|
||||
|
||||
export default AddNodeMenu;
|
@ -1,199 +0,0 @@
|
||||
import { Badge, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
||||
import { memo, useMemo } from 'react';
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getBezierPath,
|
||||
} from 'reactflow';
|
||||
import { FIELDS, colorTokenToCssVar } from '../types/constants';
|
||||
import { isInvocationNode } from '../types/types';
|
||||
|
||||
const makeEdgeSelector = (
|
||||
source: string,
|
||||
sourceHandleId: string | null | undefined,
|
||||
target: string,
|
||||
targetHandleId: string | null | undefined,
|
||||
selected?: boolean
|
||||
) =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.nodes.find((node) => node.id === target);
|
||||
|
||||
const isInvocationToInvocationEdge =
|
||||
isInvocationNode(sourceNode) && isInvocationNode(targetNode);
|
||||
|
||||
const isSelected =
|
||||
sourceNode?.selected || targetNode?.selected || selected;
|
||||
const sourceType = isInvocationToInvocationEdge
|
||||
? sourceNode?.data?.outputs[sourceHandleId || '']?.type
|
||||
: undefined;
|
||||
|
||||
const stroke =
|
||||
sourceType && nodes.shouldColorEdges
|
||||
? colorTokenToCssVar(FIELDS[sourceType].color)
|
||||
: colorTokenToCssVar('base.500');
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
|
||||
stroke,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const CollapsedEdge = memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
markerEnd,
|
||||
data,
|
||||
selected,
|
||||
source,
|
||||
target,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
}: EdgeProps<{ count: number }>) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
makeEdgeSelector(
|
||||
source,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetHandleId,
|
||||
selected
|
||||
),
|
||||
[selected, source, sourceHandleId, target, targetHandleId]
|
||||
);
|
||||
|
||||
const { isSelected, shouldAnimate } = useAppSelector(selector);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const { base500 } = useChakraThemeTokens();
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
strokeWidth: isSelected ? 3 : 2,
|
||||
stroke: base500,
|
||||
opacity: isSelected ? 0.8 : 0.5,
|
||||
animation: shouldAnimate
|
||||
? 'dashdraw 0.5s linear infinite'
|
||||
: undefined,
|
||||
strokeDasharray: shouldAnimate ? 5 : 'none',
|
||||
}}
|
||||
/>
|
||||
{data?.count && data.count > 1 && (
|
||||
<EdgeLabelRenderer>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
<Badge
|
||||
variant="solid"
|
||||
sx={{
|
||||
bg: 'base.500',
|
||||
opacity: isSelected ? 0.8 : 0.5,
|
||||
boxShadow: 'base',
|
||||
}}
|
||||
>
|
||||
{data.count}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CollapsedEdge.displayName = 'CollapsedEdge';
|
||||
|
||||
const DefaultEdge = memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
markerEnd,
|
||||
selected,
|
||||
source,
|
||||
target,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
}: EdgeProps) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
makeEdgeSelector(
|
||||
source,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetHandleId,
|
||||
selected
|
||||
),
|
||||
[source, sourceHandleId, target, targetHandleId, selected]
|
||||
);
|
||||
|
||||
const { isSelected, shouldAnimate, stroke } = useAppSelector(selector);
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
strokeWidth: isSelected ? 3 : 2,
|
||||
stroke,
|
||||
opacity: isSelected ? 0.8 : 0.5,
|
||||
animation: shouldAnimate
|
||||
? 'dashdraw 0.5s linear infinite'
|
||||
: undefined,
|
||||
strokeDasharray: shouldAnimate ? 5 : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DefaultEdge.displayName = 'DefaultEdge';
|
||||
|
||||
export const edgeTypes = {
|
||||
collapsed: CollapsedEdge,
|
||||
default: DefaultEdge,
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import CurrentImageNode from './nodes/CurrentImageNode';
|
||||
import InvocationNodeWrapper from './nodes/InvocationNodeWrapper';
|
||||
import NotesNode from './nodes/NotesNode';
|
||||
|
||||
export const nodeTypes = {
|
||||
invocation: InvocationNodeWrapper,
|
||||
current_image: CurrentImageNode,
|
||||
notes: NotesNode,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user