mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
ui: gallery enhancements (#3752)
* feat(ui): salvaged gallery UI enhancements * restore boardimage functionality, load boardimages and remove some cachine optimizations in the name of data integrity * fix assets, fix load more params * jk NOW fix assets, fix load more params --------- Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local> Co-authored-by: Mary Hipp Rogers <maryhipp@gmail.com>
This commit is contained in:
parent
271f64068c
commit
536a397b12
@ -36,6 +36,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-empty-interface': [
|
'@typescript-eslint/no-empty-interface': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
"pinGallery": "Pin Gallery",
|
"pinGallery": "Pin Gallery",
|
||||||
"allImagesLoaded": "All Images Loaded",
|
"allImagesLoaded": "All Images Loaded",
|
||||||
"loadMore": "Load More",
|
"loadMore": "Load More",
|
||||||
"noImagesInGallery": "No Images In Gallery",
|
"noImagesInGallery": "No Images to Display",
|
||||||
"deleteImage": "Delete Image",
|
"deleteImage": "Delete Image",
|
||||||
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
|
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
|
||||||
"deleteImagePermanent": "Deleted images cannot be restored.",
|
"deleteImagePermanent": "Deleted images cannot be restored.",
|
||||||
|
@ -6,9 +6,7 @@ import { PartialAppConfig } from 'app/types/invokeai';
|
|||||||
import ImageUploader from 'common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
||||||
import DeleteImageModal from 'features/imageDeletion/components/DeleteImageModal';
|
import DeleteImageModal from 'features/imageDeletion/components/DeleteImageModal';
|
||||||
import Lightbox from 'features/lightbox/components/Lightbox';
|
|
||||||
import SiteHeader from 'features/system/components/SiteHeader';
|
import SiteHeader from 'features/system/components/SiteHeader';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||||
@ -34,8 +32,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
|
|
||||||
const log = useLogger();
|
const log = useLogger();
|
||||||
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,7 +50,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||||
{isLightboxEnabled && <Lightbox />}
|
|
||||||
<ImageUploader>
|
<ImageUploader>
|
||||||
<Grid
|
<Grid
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import { Box, ChakraProps, Flex, Heading, Image } from '@chakra-ui/react';
|
import { Box, ChakraProps, Flex, Heading, Image } 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 { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { TypesafeDraggableData } from './typesafeDnd';
|
import { TypesafeDraggableData } from './typesafeDnd';
|
||||||
|
|
||||||
@ -32,24 +28,7 @@ const STYLES: ChakraProps['sx'] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
stateSelector,
|
|
||||||
(state) => {
|
|
||||||
const gallerySelectionCount = state.gallery.selection.length;
|
|
||||||
const batchSelectionCount = state.batch.selection.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
gallerySelectionCount,
|
|
||||||
batchSelectionCount,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const DragPreview = (props: OverlayDragImageProps) => {
|
const DragPreview = (props: OverlayDragImageProps) => {
|
||||||
const { gallerySelectionCount, batchSelectionCount } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
if (!props.dragData) {
|
if (!props.dragData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -82,7 +61,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.dragData.payloadType === 'BATCH_SELECTION') {
|
if (props.dragData.payloadType === 'IMAGE_NAMES') {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -95,26 +74,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
...STYLES,
|
...STYLES,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading>{batchSelectionCount}</Heading>
|
<Heading>{props.dragData.payload.image_names.length}</Heading>
|
||||||
<Heading size="sm">Images</Heading>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.dragData.payloadType === 'GALLERY_SELECTION') {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
cursor: 'none',
|
|
||||||
userSelect: 'none',
|
|
||||||
position: 'relative',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDir: 'column',
|
|
||||||
...STYLES,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Heading>{gallerySelectionCount}</Heading>
|
|
||||||
<Heading size="sm">Images</Heading>
|
<Heading size="sm">Images</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -6,18 +6,18 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
|
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||||
|
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||||
import DragPreview from './DragPreview';
|
import DragPreview from './DragPreview';
|
||||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
} from './typesafeDnd';
|
} from './typesafeDnd';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { imageDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
|
||||||
|
|
||||||
type ImageDndContextProps = PropsWithChildren;
|
type ImageDndContextProps = PropsWithChildren;
|
||||||
|
|
||||||
@ -42,18 +42,18 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
|||||||
if (!activeData || !overData) {
|
if (!activeData || !overData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageDropped({ overData, activeData }));
|
dispatch(dndDropped({ overData, activeData }));
|
||||||
setActiveDragData(null);
|
setActiveDragData(null);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseSensor = useSensor(MouseSensor, {
|
const mouseSensor = useSensor(MouseSensor, {
|
||||||
activationConstraint: { delay: 150, tolerance: 5 },
|
activationConstraint: { distance: 10 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const touchSensor = useSensor(TouchSensor, {
|
const touchSensor = useSensor(TouchSensor, {
|
||||||
activationConstraint: { delay: 150, tolerance: 5 },
|
activationConstraint: { distance: 10 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos
|
// TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos
|
||||||
|
@ -77,18 +77,14 @@ export type ImageDraggableData = BaseDragData & {
|
|||||||
payload: { imageDTO: ImageDTO };
|
payload: { imageDTO: ImageDTO };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GallerySelectionDraggableData = BaseDragData & {
|
export type ImageNamesDraggableData = BaseDragData & {
|
||||||
payloadType: 'GALLERY_SELECTION';
|
payloadType: 'IMAGE_NAMES';
|
||||||
};
|
payload: { image_names: string[] };
|
||||||
|
|
||||||
export type BatchSelectionDraggableData = BaseDragData & {
|
|
||||||
payloadType: 'BATCH_SELECTION';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDraggableData =
|
export type TypesafeDraggableData =
|
||||||
| ImageDraggableData
|
| ImageDraggableData
|
||||||
| GallerySelectionDraggableData
|
| ImageNamesDraggableData;
|
||||||
| BatchSelectionDraggableData;
|
|
||||||
|
|
||||||
interface UseDroppableTypesafeArguments
|
interface UseDroppableTypesafeArguments
|
||||||
extends Omit<UseDroppableArguments, 'data'> {
|
extends Omit<UseDroppableArguments, 'data'> {
|
||||||
@ -159,13 +155,11 @@ export const isValidDrop = (
|
|||||||
case 'SET_NODES_IMAGE':
|
case 'SET_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'SET_MULTI_NODES_IMAGE':
|
case 'SET_MULTI_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||||
case 'ADD_TO_BATCH':
|
case 'ADD_TO_BATCH':
|
||||||
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||||
case 'MOVE_BOARD':
|
case 'MOVE_BOARD':
|
||||||
return (
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||||
payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION' || 'BATCH_SELECTION'
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
// import { createAction } from '@reduxjs/toolkit';
|
|
||||||
// import * as InvokeAI from 'app/types/invokeai';
|
|
||||||
// import { GalleryCategory } from 'features/gallery/store/gallerySlice';
|
|
||||||
// import { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * We can't use redux-toolkit's createSlice() to make these actions,
|
|
||||||
// * because they have no associated reducer. They only exist to dispatch
|
|
||||||
// * requests to the server via socketio. These actions will be handled
|
|
||||||
// * by the middleware.
|
|
||||||
// */
|
|
||||||
|
|
||||||
// export const generateImage = createAction<InvokeTabName>(
|
|
||||||
// 'socketio/generateImage'
|
|
||||||
// );
|
|
||||||
// export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
|
|
||||||
// export const runFacetool = createAction<InvokeAI._Image>(
|
|
||||||
// 'socketio/runFacetool'
|
|
||||||
// );
|
|
||||||
// export const deleteImage = createAction<InvokeAI._Image>(
|
|
||||||
// 'socketio/deleteImage'
|
|
||||||
// );
|
|
||||||
// export const requestImages = createAction<GalleryCategory>(
|
|
||||||
// 'socketio/requestImages'
|
|
||||||
// );
|
|
||||||
// export const requestNewImages = createAction<GalleryCategory>(
|
|
||||||
// 'socketio/requestNewImages'
|
|
||||||
// );
|
|
||||||
// export const cancelProcessing = createAction<undefined>(
|
|
||||||
// 'socketio/cancelProcessing'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const requestSystemConfig = createAction<undefined>(
|
|
||||||
// 'socketio/requestSystemConfig'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const searchForModels = createAction<string>('socketio/searchForModels');
|
|
||||||
|
|
||||||
// export const addNewModel = createAction<
|
|
||||||
// InvokeAI.InvokeModelConfigProps | InvokeAI.InvokeDiffusersModelConfigProps
|
|
||||||
// >('socketio/addNewModel');
|
|
||||||
|
|
||||||
// export const deleteModel = createAction<string>('socketio/deleteModel');
|
|
||||||
|
|
||||||
// export const convertToDiffusers =
|
|
||||||
// createAction<InvokeAI.InvokeModelConversionProps>(
|
|
||||||
// 'socketio/convertToDiffusers'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const mergeDiffusersModels =
|
|
||||||
// createAction<InvokeAI.InvokeModelMergingProps>(
|
|
||||||
// 'socketio/mergeDiffusersModels'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const requestModelChange = createAction<string>(
|
|
||||||
// 'socketio/requestModelChange'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const saveStagingAreaImageToGallery = createAction<string>(
|
|
||||||
// 'socketio/saveStagingAreaImageToGallery'
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const emptyTempFolder = createAction<undefined>(
|
|
||||||
// 'socketio/requestEmptyTempFolder'
|
|
||||||
// );
|
|
||||||
|
|
||||||
export default {};
|
|
@ -1,209 +0,0 @@
|
|||||||
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
|
||||||
import * as InvokeAI from 'app/types/invokeai';
|
|
||||||
import type { RootState } from 'app/store/store';
|
|
||||||
import {
|
|
||||||
frontendToBackendParameters,
|
|
||||||
FrontendToBackendParametersConfig,
|
|
||||||
} from 'common/util/parameterTranslation';
|
|
||||||
import dateFormat from 'dateformat';
|
|
||||||
import {
|
|
||||||
GalleryCategory,
|
|
||||||
GalleryState,
|
|
||||||
removeImage,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import {
|
|
||||||
generationRequested,
|
|
||||||
modelChangeRequested,
|
|
||||||
modelConvertRequested,
|
|
||||||
modelMergingRequested,
|
|
||||||
setIsProcessing,
|
|
||||||
} from 'features/system/store/systemSlice';
|
|
||||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object containing all functions which use `socketio.emit()`.
|
|
||||||
* i.e. those which make server requests.
|
|
||||||
*/
|
|
||||||
const makeSocketIOEmitters = (
|
|
||||||
store: MiddlewareAPI<Dispatch<AnyAction>, RootState>,
|
|
||||||
socketio: Socket
|
|
||||||
) => {
|
|
||||||
// We need to dispatch actions to redux and get pieces of state from the store.
|
|
||||||
const { dispatch, getState } = store;
|
|
||||||
|
|
||||||
return {
|
|
||||||
emitGenerateImage: (generationMode: InvokeTabName) => {
|
|
||||||
dispatch(setIsProcessing(true));
|
|
||||||
|
|
||||||
const state: RootState = getState();
|
|
||||||
|
|
||||||
const {
|
|
||||||
generation: generationState,
|
|
||||||
postprocessing: postprocessingState,
|
|
||||||
system: systemState,
|
|
||||||
canvas: canvasState,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
|
|
||||||
{
|
|
||||||
generationMode,
|
|
||||||
generationState,
|
|
||||||
postprocessingState,
|
|
||||||
canvasState,
|
|
||||||
systemState,
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(generationRequested());
|
|
||||||
|
|
||||||
const { generationParameters, esrganParameters, facetoolParameters } =
|
|
||||||
frontendToBackendParameters(frontendToBackendParametersConfig);
|
|
||||||
|
|
||||||
socketio.emit(
|
|
||||||
'generateImage',
|
|
||||||
generationParameters,
|
|
||||||
esrganParameters,
|
|
||||||
facetoolParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
// we need to truncate the init_mask base64 else it takes up the whole log
|
|
||||||
// TODO: handle maintaining masks for reproducibility in future
|
|
||||||
if (generationParameters.init_mask) {
|
|
||||||
generationParameters.init_mask = generationParameters.init_mask
|
|
||||||
.substr(0, 64)
|
|
||||||
.concat('...');
|
|
||||||
}
|
|
||||||
if (generationParameters.init_img) {
|
|
||||||
generationParameters.init_img = generationParameters.init_img
|
|
||||||
.substr(0, 64)
|
|
||||||
.concat('...');
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: `Image generation requested: ${JSON.stringify({
|
|
||||||
...generationParameters,
|
|
||||||
...esrganParameters,
|
|
||||||
...facetoolParameters,
|
|
||||||
})}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
|
|
||||||
dispatch(setIsProcessing(true));
|
|
||||||
|
|
||||||
const {
|
|
||||||
postprocessing: {
|
|
||||||
upscalingLevel,
|
|
||||||
upscalingDenoising,
|
|
||||||
upscalingStrength,
|
|
||||||
},
|
|
||||||
} = getState();
|
|
||||||
|
|
||||||
const esrganParameters = {
|
|
||||||
upscale: [upscalingLevel, upscalingDenoising, upscalingStrength],
|
|
||||||
};
|
|
||||||
socketio.emit('runPostprocessing', imageToProcess, {
|
|
||||||
type: 'esrgan',
|
|
||||||
...esrganParameters,
|
|
||||||
});
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: `ESRGAN upscale requested: ${JSON.stringify({
|
|
||||||
file: imageToProcess.url,
|
|
||||||
...esrganParameters,
|
|
||||||
})}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
|
|
||||||
dispatch(setIsProcessing(true));
|
|
||||||
|
|
||||||
const {
|
|
||||||
postprocessing: { facetoolType, facetoolStrength, codeformerFidelity },
|
|
||||||
} = getState();
|
|
||||||
|
|
||||||
const facetoolParameters: Record<string, unknown> = {
|
|
||||||
facetool_strength: facetoolStrength,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (facetoolType === 'codeformer') {
|
|
||||||
facetoolParameters.codeformer_fidelity = codeformerFidelity;
|
|
||||||
}
|
|
||||||
|
|
||||||
socketio.emit('runPostprocessing', imageToProcess, {
|
|
||||||
type: facetoolType,
|
|
||||||
...facetoolParameters,
|
|
||||||
});
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: `Face restoration (${facetoolType}) requested: ${JSON.stringify(
|
|
||||||
{
|
|
||||||
file: imageToProcess.url,
|
|
||||||
...facetoolParameters,
|
|
||||||
}
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
|
|
||||||
const { url, uuid, category, thumbnail } = imageToDelete;
|
|
||||||
dispatch(removeImage(imageToDelete));
|
|
||||||
socketio.emit('deleteImage', url, thumbnail, uuid, category);
|
|
||||||
},
|
|
||||||
emitRequestImages: (category: GalleryCategory) => {
|
|
||||||
const gallery: GalleryState = getState().gallery;
|
|
||||||
const { earliest_mtime } = gallery.categories[category];
|
|
||||||
socketio.emit('requestImages', category, earliest_mtime);
|
|
||||||
},
|
|
||||||
emitRequestNewImages: (category: GalleryCategory) => {
|
|
||||||
const gallery: GalleryState = getState().gallery;
|
|
||||||
const { latest_mtime } = gallery.categories[category];
|
|
||||||
socketio.emit('requestLatestImages', category, latest_mtime);
|
|
||||||
},
|
|
||||||
emitCancelProcessing: () => {
|
|
||||||
socketio.emit('cancel');
|
|
||||||
},
|
|
||||||
emitRequestSystemConfig: () => {
|
|
||||||
socketio.emit('requestSystemConfig');
|
|
||||||
},
|
|
||||||
emitSearchForModels: (modelFolder: string) => {
|
|
||||||
socketio.emit('searchForModels', modelFolder);
|
|
||||||
},
|
|
||||||
emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => {
|
|
||||||
socketio.emit('addNewModel', modelConfig);
|
|
||||||
},
|
|
||||||
emitDeleteModel: (modelName: string) => {
|
|
||||||
socketio.emit('deleteModel', modelName);
|
|
||||||
},
|
|
||||||
emitConvertToDiffusers: (
|
|
||||||
modelToConvert: InvokeAI.InvokeModelConversionProps
|
|
||||||
) => {
|
|
||||||
dispatch(modelConvertRequested());
|
|
||||||
socketio.emit('convertToDiffusers', modelToConvert);
|
|
||||||
},
|
|
||||||
emitMergeDiffusersModels: (
|
|
||||||
modelMergeInfo: InvokeAI.InvokeModelMergingProps
|
|
||||||
) => {
|
|
||||||
dispatch(modelMergingRequested());
|
|
||||||
socketio.emit('mergeDiffusersModels', modelMergeInfo);
|
|
||||||
},
|
|
||||||
emitRequestModelChange: (modelName: string) => {
|
|
||||||
dispatch(modelChangeRequested());
|
|
||||||
socketio.emit('requestModelChange', modelName);
|
|
||||||
},
|
|
||||||
emitSaveStagingAreaImageToGallery: (url: string) => {
|
|
||||||
socketio.emit('requestSaveStagingAreaImageToGallery', url);
|
|
||||||
},
|
|
||||||
emitRequestEmptyTempFolder: () => {
|
|
||||||
socketio.emit('requestEmptyTempFolder');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default makeSocketIOEmitters;
|
|
||||||
|
|
||||||
export default {};
|
|
@ -1,502 +0,0 @@
|
|||||||
// import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
|
||||||
// import dateFormat from 'dateformat';
|
|
||||||
// import i18n from 'i18n';
|
|
||||||
// import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
// import * as InvokeAI from 'app/types/invokeai';
|
|
||||||
|
|
||||||
// import {
|
|
||||||
// addToast,
|
|
||||||
// errorOccurred,
|
|
||||||
// processingCanceled,
|
|
||||||
// setCurrentStatus,
|
|
||||||
// setFoundModels,
|
|
||||||
// setIsCancelable,
|
|
||||||
// setIsConnected,
|
|
||||||
// setIsProcessing,
|
|
||||||
// setModelList,
|
|
||||||
// setSearchFolder,
|
|
||||||
// setSystemConfig,
|
|
||||||
// setSystemStatus,
|
|
||||||
// } from 'features/system/store/systemSlice';
|
|
||||||
|
|
||||||
// import {
|
|
||||||
// addGalleryImages,
|
|
||||||
// addImage,
|
|
||||||
// clearIntermediateImage,
|
|
||||||
// GalleryState,
|
|
||||||
// removeImage,
|
|
||||||
// setIntermediateImage,
|
|
||||||
// } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
// import type { RootState } from 'app/store/store';
|
|
||||||
// import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
|
||||||
// import {
|
|
||||||
// clearInitialImage,
|
|
||||||
// initialImageSelected,
|
|
||||||
// setInfillMethod,
|
|
||||||
// // setInitialImage,
|
|
||||||
// setMaskPath,
|
|
||||||
// } from 'features/parameters/store/generationSlice';
|
|
||||||
// import { tabMap } from 'features/ui/store/tabMap';
|
|
||||||
// import {
|
|
||||||
// requestImages,
|
|
||||||
// requestNewImages,
|
|
||||||
// requestSystemConfig,
|
|
||||||
// } from './actions';
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Returns an object containing listener callbacks for socketio events.
|
|
||||||
// * TODO: This file is large, but simple. Should it be split up further?
|
|
||||||
// */
|
|
||||||
// const makeSocketIOListeners = (
|
|
||||||
// store: MiddlewareAPI<Dispatch<AnyAction>, RootState>
|
|
||||||
// ) => {
|
|
||||||
// const { dispatch, getState } = store;
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'connect' event.
|
|
||||||
// */
|
|
||||||
// onConnect: () => {
|
|
||||||
// try {
|
|
||||||
// dispatch(setIsConnected(true));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
|
|
||||||
// dispatch(requestSystemConfig());
|
|
||||||
// const gallery: GalleryState = getState().gallery;
|
|
||||||
|
|
||||||
// if (gallery.categories.result.latest_mtime) {
|
|
||||||
// dispatch(requestNewImages('result'));
|
|
||||||
// } else {
|
|
||||||
// dispatch(requestImages('result'));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (gallery.categories.user.latest_mtime) {
|
|
||||||
// dispatch(requestNewImages('user'));
|
|
||||||
// } else {
|
|
||||||
// dispatch(requestImages('user'));
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'disconnect' event.
|
|
||||||
// */
|
|
||||||
// onDisconnect: () => {
|
|
||||||
// try {
|
|
||||||
// dispatch(setIsConnected(false));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('common.statusDisconnected')));
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Disconnected from server`,
|
|
||||||
// level: 'warning',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'generationResult' event.
|
|
||||||
// */
|
|
||||||
// onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
|
||||||
// try {
|
|
||||||
// const state = getState();
|
|
||||||
// const { activeTab } = state.ui;
|
|
||||||
// const { shouldLoopback } = state.postprocessing;
|
|
||||||
// const { boundingBox: _, generationMode, ...rest } = data;
|
|
||||||
|
|
||||||
// const newImage = {
|
|
||||||
// uuid: uuidv4(),
|
|
||||||
// ...rest,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (['txt2img', 'img2img'].includes(generationMode)) {
|
|
||||||
// dispatch(
|
|
||||||
// addImage({
|
|
||||||
// category: 'result',
|
|
||||||
// image: { ...newImage, category: 'result' },
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (generationMode === 'unifiedCanvas' && data.boundingBox) {
|
|
||||||
// const { boundingBox } = data;
|
|
||||||
// dispatch(
|
|
||||||
// addImageToStagingArea({
|
|
||||||
// image: { ...newImage, category: 'temp' },
|
|
||||||
// boundingBox,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (state.canvas.shouldAutoSave) {
|
|
||||||
// dispatch(
|
|
||||||
// addImage({
|
|
||||||
// image: { ...newImage, category: 'result' },
|
|
||||||
// category: 'result',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TODO: fix
|
|
||||||
// // if (shouldLoopback) {
|
|
||||||
// // const activeTabName = tabMap[activeTab];
|
|
||||||
// // switch (activeTabName) {
|
|
||||||
// // case 'img2img': {
|
|
||||||
// // dispatch(initialImageSelected(newImage.uuid));
|
|
||||||
// // // dispatch(setInitialImage(newImage));
|
|
||||||
// // break;
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// dispatch(clearIntermediateImage());
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Image generated: ${data.url}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'intermediateResult' event.
|
|
||||||
// */
|
|
||||||
// onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
|
|
||||||
// try {
|
|
||||||
// dispatch(
|
|
||||||
// setIntermediateImage({
|
|
||||||
// uuid: uuidv4(),
|
|
||||||
// ...data,
|
|
||||||
// category: 'result',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// if (!data.isBase64) {
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Intermediate image generated: ${data.url}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive an 'esrganResult' event.
|
|
||||||
// */
|
|
||||||
// onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
|
|
||||||
// try {
|
|
||||||
// dispatch(
|
|
||||||
// addImage({
|
|
||||||
// category: 'result',
|
|
||||||
// image: {
|
|
||||||
// uuid: uuidv4(),
|
|
||||||
// ...data,
|
|
||||||
// category: 'result',
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Postprocessed: ${data.url}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'progressUpdate' event.
|
|
||||||
// * TODO: Add additional progress phases
|
|
||||||
// */
|
|
||||||
// onProgressUpdate: (data: InvokeAI.SystemStatus) => {
|
|
||||||
// try {
|
|
||||||
// dispatch(setIsProcessing(true));
|
|
||||||
// dispatch(setSystemStatus(data));
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'progressUpdate' event.
|
|
||||||
// */
|
|
||||||
// onError: (data: InvokeAI.ErrorResponse) => {
|
|
||||||
// const { message, additionalData } = data;
|
|
||||||
|
|
||||||
// if (additionalData) {
|
|
||||||
// // TODO: handle more data than short message
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Server error: ${message}`,
|
|
||||||
// level: 'error',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(errorOccurred());
|
|
||||||
// dispatch(clearIntermediateImage());
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'galleryImages' event.
|
|
||||||
// */
|
|
||||||
// onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
|
|
||||||
// const { images, areMoreImagesAvailable, category } = data;
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * the logic here ideally would be in the reducer but we have a side effect:
|
|
||||||
// * generating a uuid. so the logic needs to be here, outside redux.
|
|
||||||
// */
|
|
||||||
|
|
||||||
// // Generate a UUID for each image
|
|
||||||
// const preparedImages = images.map((image): InvokeAI._Image => {
|
|
||||||
// return {
|
|
||||||
// uuid: uuidv4(),
|
|
||||||
// ...image,
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addGalleryImages({
|
|
||||||
// images: preparedImages,
|
|
||||||
// areMoreImagesAvailable,
|
|
||||||
// category,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Loaded ${images.length} images`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'processingCanceled' event.
|
|
||||||
// */
|
|
||||||
// onProcessingCanceled: () => {
|
|
||||||
// dispatch(processingCanceled());
|
|
||||||
|
|
||||||
// const { intermediateImage } = getState().gallery;
|
|
||||||
|
|
||||||
// if (intermediateImage) {
|
|
||||||
// if (!intermediateImage.isBase64) {
|
|
||||||
// dispatch(
|
|
||||||
// addImage({
|
|
||||||
// category: 'result',
|
|
||||||
// image: intermediateImage,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Intermediate image saved: ${intermediateImage.url}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// dispatch(clearIntermediateImage());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Processing canceled`,
|
|
||||||
// level: 'warning',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// /**
|
|
||||||
// * Callback to run when we receive a 'imageDeleted' event.
|
|
||||||
// */
|
|
||||||
// onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
|
||||||
// const { url } = data;
|
|
||||||
|
|
||||||
// // remove image from gallery
|
|
||||||
// dispatch(removeImage(data));
|
|
||||||
|
|
||||||
// // remove references to image in options
|
|
||||||
// const {
|
|
||||||
// generation: { initialImage, maskPath },
|
|
||||||
// } = getState();
|
|
||||||
|
|
||||||
// if (
|
|
||||||
// initialImage === url ||
|
|
||||||
// (initialImage as InvokeAI._Image)?.url === url
|
|
||||||
// ) {
|
|
||||||
// dispatch(clearInitialImage());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (maskPath === url) {
|
|
||||||
// dispatch(setMaskPath(''));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Image deleted: ${url}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onSystemConfig: (data: InvokeAI.SystemConfig) => {
|
|
||||||
// dispatch(setSystemConfig(data));
|
|
||||||
// if (!data.infill_methods.includes('patchmatch')) {
|
|
||||||
// dispatch(setInfillMethod(data.infill_methods[0]));
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// onFoundModels: (data: InvokeAI.FoundModelResponse) => {
|
|
||||||
// const { search_folder, found_models } = data;
|
|
||||||
// dispatch(setSearchFolder(search_folder));
|
|
||||||
// dispatch(setFoundModels(found_models));
|
|
||||||
// },
|
|
||||||
// onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => {
|
|
||||||
// const { new_model_name, model_list, update } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('modelManager.modelAdded')));
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Model Added: ${new_model_name}`,
|
|
||||||
// level: 'info',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(
|
|
||||||
// addToast({
|
|
||||||
// title: !update
|
|
||||||
// ? `${i18n.t('modelManager.modelAdded')}: ${new_model_name}`
|
|
||||||
// : `${i18n.t('modelManager.modelUpdated')}: ${new_model_name}`,
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => {
|
|
||||||
// const { deleted_model_name, model_list } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `${i18n.t(
|
|
||||||
// 'modelManager.modelAdded'
|
|
||||||
// )}: ${deleted_model_name}`,
|
|
||||||
// level: 'info',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(
|
|
||||||
// addToast({
|
|
||||||
// title: `${i18n.t(
|
|
||||||
// 'modelManager.modelEntryDeleted'
|
|
||||||
// )}: ${deleted_model_name}`,
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onModelConverted: (data: InvokeAI.ModelConvertedResponse) => {
|
|
||||||
// const { converted_model_name, model_list } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('common.statusModelConverted')));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(setIsCancelable(true));
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Model converted: ${converted_model_name}`,
|
|
||||||
// level: 'info',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(
|
|
||||||
// addToast({
|
|
||||||
// title: `${i18n.t(
|
|
||||||
// 'modelManager.modelConverted'
|
|
||||||
// )}: ${converted_model_name}`,
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onModelsMerged: (data: InvokeAI.ModelsMergedResponse) => {
|
|
||||||
// const { merged_models, merged_model_name, model_list } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('common.statusMergedModels')));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(setIsCancelable(true));
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Models merged: ${merged_models}`,
|
|
||||||
// level: 'info',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// dispatch(
|
|
||||||
// addToast({
|
|
||||||
// title: `${i18n.t('modelManager.modelsMerged')}: ${merged_model_name}`,
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
|
||||||
// const { model_name, model_list } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setCurrentStatus(i18n.t('common.statusModelChanged')));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(setIsCancelable(true));
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Model changed: ${model_name}`,
|
|
||||||
// level: 'info',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => {
|
|
||||||
// const { model_name, model_list } = data;
|
|
||||||
// dispatch(setModelList(model_list));
|
|
||||||
// dispatch(setIsProcessing(false));
|
|
||||||
// dispatch(setIsCancelable(true));
|
|
||||||
// dispatch(errorOccurred());
|
|
||||||
// dispatch(
|
|
||||||
// addLogEntry({
|
|
||||||
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
// message: `Model change failed: ${model_name}`,
|
|
||||||
// level: 'error',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// onTempFolderEmptied: () => {
|
|
||||||
// dispatch(
|
|
||||||
// addToast({
|
|
||||||
// title: i18n.t('toast.tempFoldersEmptied'),
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default makeSocketIOListeners;
|
|
||||||
|
|
||||||
export default {};
|
|
@ -1,248 +0,0 @@
|
|||||||
// import { Middleware } from '@reduxjs/toolkit';
|
|
||||||
// import { io } from 'socket.io-client';
|
|
||||||
|
|
||||||
// import makeSocketIOEmitters from './emitters';
|
|
||||||
// import makeSocketIOListeners from './listeners';
|
|
||||||
|
|
||||||
// import * as InvokeAI from 'app/types/invokeai';
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Creates a socketio middleware to handle communication with server.
|
|
||||||
// *
|
|
||||||
// * Special `socketio/actionName` actions are created in actions.ts and
|
|
||||||
// * exported for use by the application, which treats them like any old
|
|
||||||
// * action, using `dispatch` to dispatch them.
|
|
||||||
// *
|
|
||||||
// * These actions are intercepted here, where `socketio.emit()` calls are
|
|
||||||
// * made on their behalf - see `emitters.ts`. The emitter functions
|
|
||||||
// * are the outbound communication to the server.
|
|
||||||
// *
|
|
||||||
// * Listeners are also established here - see `listeners.ts`. The listener
|
|
||||||
// * functions receive communication from the server and usually dispatch
|
|
||||||
// * some new action to handle whatever data was sent from the server.
|
|
||||||
// */
|
|
||||||
// export const socketioMiddleware = () => {
|
|
||||||
// const { origin } = new URL(window.location.href);
|
|
||||||
|
|
||||||
// const socketio = io(origin, {
|
|
||||||
// timeout: 60000,
|
|
||||||
// path: `${window.location.pathname}socket.io`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.disconnect();
|
|
||||||
|
|
||||||
// let areListenersSet = false;
|
|
||||||
|
|
||||||
// const middleware: Middleware = (store) => (next) => (action) => {
|
|
||||||
// const {
|
|
||||||
// onConnect,
|
|
||||||
// onDisconnect,
|
|
||||||
// onError,
|
|
||||||
// onPostprocessingResult,
|
|
||||||
// onGenerationResult,
|
|
||||||
// onIntermediateResult,
|
|
||||||
// onProgressUpdate,
|
|
||||||
// onGalleryImages,
|
|
||||||
// onProcessingCanceled,
|
|
||||||
// onImageDeleted,
|
|
||||||
// onSystemConfig,
|
|
||||||
// onModelChanged,
|
|
||||||
// onFoundModels,
|
|
||||||
// onNewModelAdded,
|
|
||||||
// onModelDeleted,
|
|
||||||
// onModelConverted,
|
|
||||||
// onModelsMerged,
|
|
||||||
// onModelChangeFailed,
|
|
||||||
// onTempFolderEmptied,
|
|
||||||
// } = makeSocketIOListeners(store);
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// emitGenerateImage,
|
|
||||||
// emitRunESRGAN,
|
|
||||||
// emitRunFacetool,
|
|
||||||
// emitDeleteImage,
|
|
||||||
// emitRequestImages,
|
|
||||||
// emitRequestNewImages,
|
|
||||||
// emitCancelProcessing,
|
|
||||||
// emitRequestSystemConfig,
|
|
||||||
// emitSearchForModels,
|
|
||||||
// emitAddNewModel,
|
|
||||||
// emitDeleteModel,
|
|
||||||
// emitConvertToDiffusers,
|
|
||||||
// emitMergeDiffusersModels,
|
|
||||||
// emitRequestModelChange,
|
|
||||||
// emitSaveStagingAreaImageToGallery,
|
|
||||||
// emitRequestEmptyTempFolder,
|
|
||||||
// } = makeSocketIOEmitters(store, socketio);
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * If this is the first time the middleware has been called (e.g. during store setup),
|
|
||||||
// * initialize all our socket.io listeners.
|
|
||||||
// */
|
|
||||||
// if (!areListenersSet) {
|
|
||||||
// socketio.on('connect', () => onConnect());
|
|
||||||
|
|
||||||
// socketio.on('disconnect', () => onDisconnect());
|
|
||||||
|
|
||||||
// socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data));
|
|
||||||
|
|
||||||
// socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) =>
|
|
||||||
// onGenerationResult(data)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// socketio.on(
|
|
||||||
// 'postprocessingResult',
|
|
||||||
// (data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
|
|
||||||
// onIntermediateResult(data)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) =>
|
|
||||||
// onProgressUpdate(data)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) =>
|
|
||||||
// onGalleryImages(data)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// socketio.on('processingCanceled', () => {
|
|
||||||
// onProcessingCanceled();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
|
|
||||||
// onImageDeleted(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
|
|
||||||
// onSystemConfig(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => {
|
|
||||||
// onFoundModels(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => {
|
|
||||||
// onNewModelAdded(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => {
|
|
||||||
// onModelDeleted(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => {
|
|
||||||
// onModelConverted(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => {
|
|
||||||
// onModelsMerged(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
|
|
||||||
// onModelChanged(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => {
|
|
||||||
// onModelChangeFailed(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketio.on('tempFolderEmptied', () => {
|
|
||||||
// onTempFolderEmptied();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// areListenersSet = true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Handle redux actions caught by middleware.
|
|
||||||
// */
|
|
||||||
// switch (action.type) {
|
|
||||||
// case 'socketio/generateImage': {
|
|
||||||
// emitGenerateImage(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/runESRGAN': {
|
|
||||||
// emitRunESRGAN(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/runFacetool': {
|
|
||||||
// emitRunFacetool(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/deleteImage': {
|
|
||||||
// emitDeleteImage(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/requestImages': {
|
|
||||||
// emitRequestImages(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/requestNewImages': {
|
|
||||||
// emitRequestNewImages(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/cancelProcessing': {
|
|
||||||
// emitCancelProcessing();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/requestSystemConfig': {
|
|
||||||
// emitRequestSystemConfig();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/searchForModels': {
|
|
||||||
// emitSearchForModels(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/addNewModel': {
|
|
||||||
// emitAddNewModel(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/deleteModel': {
|
|
||||||
// emitDeleteModel(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/convertToDiffusers': {
|
|
||||||
// emitConvertToDiffusers(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/mergeDiffusersModels': {
|
|
||||||
// emitMergeDiffusersModels(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/requestModelChange': {
|
|
||||||
// emitRequestModelChange(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/saveStagingAreaImageToGallery': {
|
|
||||||
// emitSaveStagingAreaImageToGallery(action.payload);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// case 'socketio/requestEmptyTempFolder': {
|
|
||||||
// emitRequestEmptyTempFolder();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// next(action);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return middleware;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export default {};
|
|
@ -1,7 +1,6 @@
|
|||||||
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||||
import { controlNetDenylist } from 'features/controlNet/store/controlNetDenylist';
|
import { controlNetDenylist } from 'features/controlNet/store/controlNetDenylist';
|
||||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
||||||
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
|
|
||||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||||
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
||||||
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
|
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
|
||||||
@ -16,7 +15,6 @@ const serializationDenylist: {
|
|||||||
canvas: canvasPersistDenylist,
|
canvas: canvasPersistDenylist,
|
||||||
gallery: galleryPersistDenylist,
|
gallery: galleryPersistDenylist,
|
||||||
generation: generationPersistDenylist,
|
generation: generationPersistDenylist,
|
||||||
lightbox: lightboxPersistDenylist,
|
|
||||||
nodes: nodesPersistDenylist,
|
nodes: nodesPersistDenylist,
|
||||||
postprocessing: postprocessingPersistDenylist,
|
postprocessing: postprocessingPersistDenylist,
|
||||||
system: systemPersistDenylist,
|
system: systemPersistDenylist,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||||
import { initialControlNetState } from 'features/controlNet/store/controlNetSlice';
|
import { initialControlNetState } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||||
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
|
|
||||||
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
||||||
import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice';
|
import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice';
|
||||||
@ -18,7 +17,6 @@ const initialStates: {
|
|||||||
canvas: initialCanvasState,
|
canvas: initialCanvasState,
|
||||||
gallery: initialGalleryState,
|
gallery: initialGalleryState,
|
||||||
generation: initialGenerationState,
|
generation: initialGenerationState,
|
||||||
lightbox: initialLightboxState,
|
|
||||||
nodes: initialNodesState,
|
nodes: initialNodesState,
|
||||||
postprocessing: initialPostprocessingState,
|
postprocessing: initialPostprocessingState,
|
||||||
system: initialSystemState,
|
system: initialSystemState,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* This is a list of actions that should be excluded in the Redux DevTools.
|
||||||
|
*/
|
||||||
export const actionsDenylist = [
|
export const actionsDenylist = [
|
||||||
|
// very spammy canvas actions
|
||||||
'canvas/setCursorPosition',
|
'canvas/setCursorPosition',
|
||||||
'canvas/setStageCoordinates',
|
'canvas/setStageCoordinates',
|
||||||
'canvas/setStageScale',
|
'canvas/setStageScale',
|
||||||
@ -7,7 +11,11 @@ export const actionsDenylist = [
|
|||||||
'canvas/setBoundingBoxDimensions',
|
'canvas/setBoundingBoxDimensions',
|
||||||
'canvas/setIsDrawing',
|
'canvas/setIsDrawing',
|
||||||
'canvas/addPointToCurrentLine',
|
'canvas/addPointToCurrentLine',
|
||||||
|
// bazillions during generation
|
||||||
'socket/socketGeneratorProgress',
|
'socket/socketGeneratorProgress',
|
||||||
'socket/appSocketGeneratorProgress',
|
'socket/appSocketGeneratorProgress',
|
||||||
|
// every time user presses shift
|
||||||
'hotkeys/shiftKeyPressed',
|
'hotkeys/shiftKeyPressed',
|
||||||
|
// this happens after every state change
|
||||||
|
'@@REMEMBER_PERSISTED',
|
||||||
];
|
];
|
||||||
|
@ -58,7 +58,6 @@ import {
|
|||||||
addReceivedPageOfImagesFulfilledListener,
|
addReceivedPageOfImagesFulfilledListener,
|
||||||
addReceivedPageOfImagesRejectedListener,
|
addReceivedPageOfImagesRejectedListener,
|
||||||
} from './listeners/receivedPageOfImages';
|
} from './listeners/receivedPageOfImages';
|
||||||
import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
|
|
||||||
import {
|
import {
|
||||||
addSessionCanceledFulfilledListener,
|
addSessionCanceledFulfilledListener,
|
||||||
addSessionCanceledPendingListener,
|
addSessionCanceledPendingListener,
|
||||||
@ -215,9 +214,6 @@ addBoardIdSelectedListener();
|
|||||||
// Node schemas
|
// Node schemas
|
||||||
addReceivedOpenAPISchemaListener();
|
addReceivedOpenAPISchemaListener();
|
||||||
|
|
||||||
// Batches
|
|
||||||
addSelectionAddedToBatchListener();
|
|
||||||
|
|
||||||
// DND
|
// DND
|
||||||
addImageDroppedListener();
|
addImageDroppedListener();
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
INITIAL_IMAGE_LIMIT,
|
INITIAL_IMAGE_LIMIT,
|
||||||
isLoadingChanged,
|
isLoadingChanged,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
@ -20,7 +22,7 @@ export const addAppStartedListener = () => {
|
|||||||
// fill up the gallery tab with images
|
// fill up the gallery tab with images
|
||||||
await dispatch(
|
await dispatch(
|
||||||
receivedPageOfImages({
|
receivedPageOfImages({
|
||||||
categories: ['general'],
|
categories: IMAGE_CATEGORIES,
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
@ -30,7 +32,7 @@ export const addAppStartedListener = () => {
|
|||||||
// fill up the assets tab with images
|
// fill up the assets tab with images
|
||||||
await dispatch(
|
await dispatch(
|
||||||
receivedPageOfImages({
|
receivedPageOfImages({
|
||||||
categories: ['control', 'mask', 'user', 'other'],
|
categories: ASSETS_CATEGORIES,
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
|
boardIdSelected,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectImagesAll,
|
selectImagesAll,
|
||||||
boardIdSelected,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import {
|
import {
|
||||||
IMAGES_PER_PAGE,
|
IMAGES_PER_PAGE,
|
||||||
receivedPageOfImages,
|
receivedPageOfImages,
|
||||||
} from 'services/api/thunks/image';
|
} from 'services/api/thunks/image';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
|
|
||||||
@ -24,19 +27,24 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
const allImages = selectImagesAll(state);
|
const allImages = selectImagesAll(state);
|
||||||
|
|
||||||
if (!board_id) {
|
if (board_id === 'all') {
|
||||||
// a board was unselected
|
// Selected all images
|
||||||
dispatch(imageSelected(allImages[0]?.image_name));
|
dispatch(imageSelected(allImages[0]?.image_name ?? null));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { categories } = state.gallery;
|
if (board_id === 'batch') {
|
||||||
|
// Selected the batch
|
||||||
|
dispatch(imageSelected(state.gallery.batchImageNames[0] ?? null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const filteredImages = allImages.filter((i) => {
|
const filteredImages = selectFilteredImages(state);
|
||||||
const isInCategory = categories.includes(i.image_category);
|
|
||||||
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
const categories =
|
||||||
return isInCategory && isInSelectedBoard;
|
state.gallery.galleryView === 'images'
|
||||||
});
|
? IMAGE_CATEGORIES
|
||||||
|
: ASSETS_CATEGORIES;
|
||||||
|
|
||||||
// get the board from the cache
|
// get the board from the cache
|
||||||
const { data: boards } =
|
const { data: boards } =
|
||||||
@ -45,7 +53,7 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
|
|
||||||
if (!board) {
|
if (!board) {
|
||||||
// can't find the board in cache...
|
// can't find the board in cache...
|
||||||
dispatch(imageSelected(allImages[0]?.image_name));
|
dispatch(boardIdSelected('all'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,48 +71,3 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addBoardIdSelected_changeSelectedImage_listener = () => {
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: boardIdSelected,
|
|
||||||
effect: (action, { getState, dispatch }) => {
|
|
||||||
const board_id = action.payload;
|
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
// we need to check if we need to fetch more images
|
|
||||||
|
|
||||||
if (!board_id) {
|
|
||||||
// a board was unselected - we don't need to do anything
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { categories } = state.gallery;
|
|
||||||
|
|
||||||
const filteredImages = selectImagesAll(state).filter((i) => {
|
|
||||||
const isInCategory = categories.includes(i.image_category);
|
|
||||||
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
|
||||||
return isInCategory && isInSelectedBoard;
|
|
||||||
});
|
|
||||||
|
|
||||||
// get the board from the cache
|
|
||||||
const { data: boards } =
|
|
||||||
boardsApi.endpoints.listAllBoards.select()(state);
|
|
||||||
const board = boards?.find((b) => b.board_id === board_id);
|
|
||||||
if (!board) {
|
|
||||||
// can't find the board in cache...
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we haven't loaded one full page of images from this board, load more
|
|
||||||
if (
|
|
||||||
filteredImages.length < board.image_count &&
|
|
||||||
filteredImages.length < IMAGES_PER_PAGE
|
|
||||||
) {
|
|
||||||
dispatch(
|
|
||||||
receivedPageOfImages({ categories, board_id, is_intermediate: false })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
@ -15,12 +14,6 @@ export const addImageAddedToBoardFulfilledListener = () => {
|
|||||||
{ data: { board_id, image_name } },
|
{ data: { board_id, image_name } },
|
||||||
'Image added to board'
|
'Image added to board'
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(
|
|
||||||
imageDTOReceived({
|
|
||||||
image_name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||||
|
import { selectNextImageToSelect } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import {
|
||||||
imageRemoved,
|
imageRemoved,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectFilteredImages,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
imageDeletionConfirmed,
|
imageDeletionConfirmed,
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
} from 'features/imageDeletion/store/imageDeletionSlice';
|
} from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
import { api } from 'services/api';
|
import { api } from 'services/api';
|
||||||
import { imageDeleted } from 'services/api/thunks/image';
|
import { imageDeleted } from 'services/api/thunks/image';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
@ -37,26 +36,10 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
state.gallery.selection[state.gallery.selection.length - 1];
|
state.gallery.selection[state.gallery.selection.length - 1];
|
||||||
|
|
||||||
if (lastSelectedImage === image_name) {
|
if (lastSelectedImage === image_name) {
|
||||||
const filteredImages = selectFilteredImages(state);
|
const newSelectedImageId = selectNextImageToSelect(state, image_name);
|
||||||
|
|
||||||
const ids = filteredImages.map((i) => i.image_name);
|
|
||||||
|
|
||||||
const deletedImageIndex = ids.findIndex(
|
|
||||||
(result) => result.toString() === image_name
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredIds = ids.filter((id) => id.toString() !== image_name);
|
|
||||||
|
|
||||||
const newSelectedImageIndex = clamp(
|
|
||||||
deletedImageIndex,
|
|
||||||
0,
|
|
||||||
filteredIds.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
|
||||||
|
|
||||||
if (newSelectedImageId) {
|
if (newSelectedImageId) {
|
||||||
dispatch(imageSelected(newSelectedImageId as string));
|
dispatch(imageSelected(newSelectedImageId));
|
||||||
} else {
|
} else {
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelected(null));
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,12 @@ import {
|
|||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import {
|
|
||||||
imageAddedToBatch,
|
|
||||||
imagesAddedToBatch,
|
|
||||||
} from 'features/batch/store/batchSlice';
|
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import {
|
||||||
|
imageSelected,
|
||||||
|
imagesAddedToBatch,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
fieldValueChanged,
|
fieldValueChanged,
|
||||||
imageCollectionFieldValueChanged,
|
imageCollectionFieldValueChanged,
|
||||||
@ -21,57 +20,66 @@ import { startAppListening } from '../';
|
|||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'dnd' });
|
const moduleLog = log.child({ namespace: 'dnd' });
|
||||||
|
|
||||||
export const imageDropped = createAction<{
|
export const dndDropped = createAction<{
|
||||||
overData: TypesafeDroppableData;
|
overData: TypesafeDroppableData;
|
||||||
activeData: TypesafeDraggableData;
|
activeData: TypesafeDraggableData;
|
||||||
}>('dnd/imageDropped');
|
}>('dnd/dndDropped');
|
||||||
|
|
||||||
export const addImageDroppedListener = () => {
|
export const addImageDroppedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDropped,
|
actionCreator: dndDropped,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
const { activeData, overData } = action.payload;
|
const { activeData, overData } = action.payload;
|
||||||
const { actionType } = overData;
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
|
moduleLog.debug(
|
||||||
|
{ data: { activeData, overData } },
|
||||||
|
'Image or selection dropped'
|
||||||
|
);
|
||||||
|
|
||||||
// set current image
|
// set current image
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_CURRENT_IMAGE' &&
|
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(imageSelected(activeData.payload.imageDTO.image_name));
|
dispatch(imageSelected(activeData.payload.imageDTO.image_name));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set initial image
|
// set initial image
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_INITIAL_IMAGE' &&
|
overData.actionType === 'SET_INITIAL_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(initialImageChanged(activeData.payload.imageDTO));
|
dispatch(initialImageChanged(activeData.payload.imageDTO));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add image to batch
|
// add image to batch
|
||||||
if (
|
if (
|
||||||
actionType === 'ADD_TO_BATCH' &&
|
overData.actionType === 'ADD_TO_BATCH' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(imageAddedToBatch(activeData.payload.imageDTO.image_name));
|
dispatch(imagesAddedToBatch([activeData.payload.imageDTO.image_name]));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add multiple images to batch
|
// add multiple images to batch
|
||||||
if (
|
if (
|
||||||
actionType === 'ADD_TO_BATCH' &&
|
overData.actionType === 'ADD_TO_BATCH' &&
|
||||||
activeData.payloadType === 'GALLERY_SELECTION'
|
activeData.payloadType === 'IMAGE_NAMES'
|
||||||
) {
|
) {
|
||||||
dispatch(imagesAddedToBatch(state.gallery.selection));
|
dispatch(imagesAddedToBatch(activeData.payload.image_names));
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set control image
|
// set control image
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_CONTROLNET_IMAGE' &&
|
overData.actionType === 'SET_CONTROLNET_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
@ -82,20 +90,22 @@ export const addImageDroppedListener = () => {
|
|||||||
controlNetId,
|
controlNetId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set canvas image
|
// set canvas image
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(setInitialCanvasImage(activeData.payload.imageDTO));
|
dispatch(setInitialCanvasImage(activeData.payload.imageDTO));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set nodes image
|
// set nodes image
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_NODES_IMAGE' &&
|
overData.actionType === 'SET_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
@ -107,11 +117,12 @@ export const addImageDroppedListener = () => {
|
|||||||
value: activeData.payload.imageDTO,
|
value: activeData.payload.imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set multiple nodes images (single image handler)
|
// set multiple nodes images (single image handler)
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
@ -123,43 +134,30 @@ export const addImageDroppedListener = () => {
|
|||||||
value: [activeData.payload.imageDTO],
|
value: [activeData.payload.imageDTO],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set multiple nodes images (multiple images handler)
|
// set multiple nodes images (multiple images handler)
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'GALLERY_SELECTION'
|
activeData.payloadType === 'IMAGE_NAMES'
|
||||||
) {
|
) {
|
||||||
const { fieldName, nodeId } = overData.context;
|
const { fieldName, nodeId } = overData.context;
|
||||||
dispatch(
|
dispatch(
|
||||||
imageCollectionFieldValueChanged({
|
imageCollectionFieldValueChanged({
|
||||||
nodeId,
|
nodeId,
|
||||||
fieldName,
|
fieldName,
|
||||||
value: state.gallery.selection.map((image_name) => ({
|
value: activeData.payload.image_names.map((image_name) => ({
|
||||||
image_name,
|
image_name,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove image from board
|
|
||||||
// TODO: remove board_id from `removeImageFromBoard()` endpoint
|
|
||||||
// TODO: handle multiple images
|
|
||||||
// if (
|
|
||||||
// actionType === 'MOVE_BOARD' &&
|
|
||||||
// activeData.payloadType === 'IMAGE_DTO' &&
|
|
||||||
// activeData.payload.imageDTO &&
|
|
||||||
// overData.boardId !== null
|
|
||||||
// ) {
|
|
||||||
// const { image_name } = activeData.payload.imageDTO;
|
|
||||||
// dispatch(
|
|
||||||
// boardImagesApi.endpoints.removeImageFromBoard.initiate({ image_name })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// add image to board
|
// add image to board
|
||||||
if (
|
if (
|
||||||
actionType === 'MOVE_BOARD' &&
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO &&
|
activeData.payload.imageDTO &&
|
||||||
overData.context.boardId
|
overData.context.boardId
|
||||||
@ -172,17 +170,89 @@ export const addImageDroppedListener = () => {
|
|||||||
board_id: boardId,
|
board_id: boardId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add multiple images to board
|
// remove image from board
|
||||||
// TODO: add endpoint
|
if (
|
||||||
// if (
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
// actionType === 'ADD_TO_BATCH' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
activeData.payload.imageDTO &&
|
||||||
// activeData.payload.imageDTONames
|
overData.context.boardId === null
|
||||||
// ) {
|
) {
|
||||||
// dispatch(boardImagesApi.endpoints.addImagesToBoard.intiate({}));
|
const { image_name, board_id } = activeData.payload.imageDTO;
|
||||||
// }
|
if (board_id) {
|
||||||
|
dispatch(
|
||||||
|
boardImagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
|
image_name,
|
||||||
|
board_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add gallery selection to board
|
||||||
|
if (
|
||||||
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
|
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
|
overData.context.boardId
|
||||||
|
) {
|
||||||
|
console.log('adding gallery selection to board');
|
||||||
|
const board_id = overData.context.boardId;
|
||||||
|
dispatch(
|
||||||
|
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||||
|
board_id,
|
||||||
|
image_names: activeData.payload.image_names,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove gallery selection from board
|
||||||
|
if (
|
||||||
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
|
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
|
overData.context.boardId === null
|
||||||
|
) {
|
||||||
|
console.log('removing gallery selection to board');
|
||||||
|
dispatch(
|
||||||
|
boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||||
|
image_names: activeData.payload.image_names,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add batch selection to board
|
||||||
|
if (
|
||||||
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
|
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
|
overData.context.boardId
|
||||||
|
) {
|
||||||
|
const board_id = overData.context.boardId;
|
||||||
|
dispatch(
|
||||||
|
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||||
|
board_id,
|
||||||
|
image_names: activeData.payload.image_names,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove batch selection from board
|
||||||
|
if (
|
||||||
|
overData.actionType === 'MOVE_BOARD' &&
|
||||||
|
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
|
overData.context.boardId === null
|
||||||
|
) {
|
||||||
|
dispatch(
|
||||||
|
boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||||
|
image_names: activeData.payload.image_names,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
@ -15,12 +14,6 @@ export const addImageRemovedFromBoardFulfilledListener = () => {
|
|||||||
{ data: { board_id, image_name } },
|
{ data: { board_id, image_name } },
|
||||||
'Image added to board'
|
'Image added to board'
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(
|
|
||||||
imageDTOReceived({
|
|
||||||
image_name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { startAppListening } from '..';
|
|
||||||
import { imageUploaded } from 'services/api/thunks/image';
|
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import {
|
||||||
|
imageUpserted,
|
||||||
|
imagesAddedToBatch,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { imageAddedToBatch } from 'features/batch/store/batchSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { imageUploaded } from 'services/api/thunks/image';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -73,7 +75,7 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'ADD_TO_BATCH') {
|
if (postUploadAction?.type === 'ADD_TO_BATCH') {
|
||||||
dispatch(imageAddedToBatch(image.image_name));
|
dispatch(imagesAddedToBatch([image.image_name]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { startAppListening } from '..';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
|
||||||
import {
|
|
||||||
imagesAddedToBatch,
|
|
||||||
selectionAddedToBatch,
|
|
||||||
} from 'features/batch/store/batchSlice';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'batch' });
|
|
||||||
|
|
||||||
export const addSelectionAddedToBatchListener = () => {
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: selectionAddedToBatch,
|
|
||||||
effect: (action, { dispatch, getState }) => {
|
|
||||||
const { selection } = getState().gallery;
|
|
||||||
|
|
||||||
dispatch(imagesAddedToBatch(selection));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
@ -9,14 +9,12 @@ import {
|
|||||||
import dynamicMiddlewares from 'redux-dynamic-middlewares';
|
import dynamicMiddlewares from 'redux-dynamic-middlewares';
|
||||||
import { rememberEnhancer, rememberReducer } from 'redux-remember';
|
import { rememberEnhancer, rememberReducer } from 'redux-remember';
|
||||||
|
|
||||||
import batchReducer from 'features/batch/store/batchSlice';
|
|
||||||
import canvasReducer from 'features/canvas/store/canvasSlice';
|
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||||
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
|
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
|
||||||
import dynamicPromptsReducer from 'features/dynamicPrompts/store/slice';
|
import dynamicPromptsReducer from 'features/dynamicPrompts/store/slice';
|
||||||
import boardsReducer from 'features/gallery/store/boardSlice';
|
import boardsReducer from 'features/gallery/store/boardSlice';
|
||||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||||
import imageDeletionReducer from 'features/imageDeletion/store/imageDeletionSlice';
|
import imageDeletionReducer from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
|
||||||
import loraReducer from 'features/lora/store/loraSlice';
|
import loraReducer from 'features/lora/store/loraSlice';
|
||||||
import nodesReducer from 'features/nodes/store/nodesSlice';
|
import nodesReducer from 'features/nodes/store/nodesSlice';
|
||||||
import generationReducer from 'features/parameters/store/generationSlice';
|
import generationReducer from 'features/parameters/store/generationSlice';
|
||||||
@ -40,7 +38,6 @@ const allReducers = {
|
|||||||
canvas: canvasReducer,
|
canvas: canvasReducer,
|
||||||
gallery: galleryReducer,
|
gallery: galleryReducer,
|
||||||
generation: generationReducer,
|
generation: generationReducer,
|
||||||
lightbox: lightboxReducer,
|
|
||||||
nodes: nodesReducer,
|
nodes: nodesReducer,
|
||||||
postprocessing: postprocessingReducer,
|
postprocessing: postprocessingReducer,
|
||||||
system: systemReducer,
|
system: systemReducer,
|
||||||
@ -50,7 +47,6 @@ const allReducers = {
|
|||||||
controlNet: controlNetReducer,
|
controlNet: controlNetReducer,
|
||||||
boards: boardsReducer,
|
boards: boardsReducer,
|
||||||
dynamicPrompts: dynamicPromptsReducer,
|
dynamicPrompts: dynamicPromptsReducer,
|
||||||
batch: batchReducer,
|
|
||||||
imageDeletion: imageDeletionReducer,
|
imageDeletion: imageDeletionReducer,
|
||||||
lora: loraReducer,
|
lora: loraReducer,
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
@ -64,18 +60,13 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
|
|||||||
'canvas',
|
'canvas',
|
||||||
'gallery',
|
'gallery',
|
||||||
'generation',
|
'generation',
|
||||||
'lightbox',
|
|
||||||
'nodes',
|
'nodes',
|
||||||
'postprocessing',
|
'postprocessing',
|
||||||
'system',
|
'system',
|
||||||
'ui',
|
'ui',
|
||||||
'controlNet',
|
'controlNet',
|
||||||
'dynamicPrompts',
|
'dynamicPrompts',
|
||||||
'batch',
|
|
||||||
'lora',
|
'lora',
|
||||||
// 'boards',
|
|
||||||
// 'hotkeys',
|
|
||||||
// 'config',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
@ -101,10 +92,26 @@ export const store = configureStore({
|
|||||||
.concat(dynamicMiddlewares)
|
.concat(dynamicMiddlewares)
|
||||||
.prepend(listenerMiddleware.middleware),
|
.prepend(listenerMiddleware.middleware),
|
||||||
devTools: {
|
devTools: {
|
||||||
actionsDenylist,
|
|
||||||
actionSanitizer,
|
actionSanitizer,
|
||||||
stateSanitizer,
|
stateSanitizer,
|
||||||
trace: true,
|
trace: true,
|
||||||
|
predicate: (state, action) => {
|
||||||
|
// TODO: hook up to the log level param in system slice
|
||||||
|
// manually type state, cannot type the arg
|
||||||
|
// const typedState = state as ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
|
if (action.type.startsWith('api/')) {
|
||||||
|
// don't log api actions, with manual cache updates they are extremely noisy
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionsDenylist.includes(action.type)) {
|
||||||
|
// don't log other noisy actions
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -94,7 +94,8 @@ export type AppFeature =
|
|||||||
| 'bugLink'
|
| 'bugLink'
|
||||||
| 'localization'
|
| 'localization'
|
||||||
| 'consoleLogging'
|
| 'consoleLogging'
|
||||||
| 'dynamicPrompting';
|
| 'dynamicPrompting'
|
||||||
|
| 'batches';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A disable-able Stable Diffusion feature
|
* A disable-able Stable Diffusion feature
|
||||||
|
@ -6,30 +6,21 @@ import {
|
|||||||
useColorMode,
|
useColorMode,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useCombinedRefs } from '@dnd-kit/utilities';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import {
|
|
||||||
IAILoadingImageFallback,
|
|
||||||
IAINoContentFallback,
|
|
||||||
} from 'common/components/IAIImageFallback';
|
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import { MouseEvent, ReactElement, SyntheticEvent } from 'react';
|
|
||||||
import { memo, useRef } from 'react';
|
|
||||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import IAIDropOverlay from './IAIDropOverlay';
|
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
import {
|
import {
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
isValidDrop,
|
|
||||||
useDraggable,
|
|
||||||
useDroppable,
|
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
||||||
|
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
|
import { PostUploadAction } from 'services/api/thunks/image';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { mode } from 'theme/util/mode';
|
||||||
|
import IAIDraggable from './IAIDraggable';
|
||||||
|
import IAIDroppable from './IAIDroppable';
|
||||||
|
|
||||||
type IAIDndImageProps = {
|
type IAIDndImageProps = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
@ -83,28 +74,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
const dndId = useRef(uuidv4());
|
|
||||||
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef: setDraggableRef,
|
|
||||||
isDragging,
|
|
||||||
active,
|
|
||||||
} = useDraggable({
|
|
||||||
id: dndId.current,
|
|
||||||
disabled: isDragDisabled || !imageDTO,
|
|
||||||
data: draggableData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
|
||||||
id: dndId.current,
|
|
||||||
disabled: isDropDisabled,
|
|
||||||
data: droppableData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setDndRef = useCombinedRefs(setDroppableRef, setDraggableRef);
|
|
||||||
|
|
||||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
isDisabled: isUploadDisabled,
|
isDisabled: isUploadDisabled,
|
||||||
@ -139,9 +108,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
|
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
|
||||||
}}
|
}}
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
ref={setDndRef}
|
|
||||||
>
|
>
|
||||||
{imageDTO && (
|
{imageDTO && (
|
||||||
<Flex
|
<Flex
|
||||||
@ -154,10 +120,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
onClick={onClick}
|
|
||||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||||
fallbackStrategy="beforeLoadOrError"
|
fallbackStrategy="beforeLoadOrError"
|
||||||
fallback={<IAILoadingImageFallback image={imageDTO} />}
|
// If we fall back to thumbnail, it feels much snappier than the skeleton...
|
||||||
|
fallbackSrc={imageDTO.thumbnail_url}
|
||||||
|
// fallback={<IAILoadingImageFallback image={imageDTO} />}
|
||||||
|
width={imageDTO.width}
|
||||||
|
height={imageDTO.height}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
sx={{
|
sx={{
|
||||||
@ -171,30 +140,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
||||||
{onClickReset && withResetIcon && (
|
|
||||||
<IAIIconButton
|
|
||||||
onClick={onClickReset}
|
|
||||||
aria-label={resetTooltip}
|
|
||||||
tooltip={resetTooltip}
|
|
||||||
icon={resetIcon}
|
|
||||||
size="sm"
|
|
||||||
variant="link"
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 1,
|
|
||||||
insetInlineEnd: 1,
|
|
||||||
p: 0,
|
|
||||||
minW: 0,
|
|
||||||
svg: {
|
|
||||||
transitionProperty: 'common',
|
|
||||||
transitionDuration: 'normal',
|
|
||||||
fill: 'base.100',
|
|
||||||
_hover: { fill: 'base.50' },
|
|
||||||
filter: resetIconShadow,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && !isUploadDisabled && (
|
{!imageDTO && !isUploadDisabled && (
|
||||||
@ -225,11 +170,42 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||||
<AnimatePresence>
|
<IAIDroppable
|
||||||
{isValidDrop(droppableData, active) && !isDragging && (
|
data={droppableData}
|
||||||
<IAIDropOverlay isOver={isOver} label={dropLabel} />
|
disabled={isDropDisabled}
|
||||||
)}
|
dropLabel={dropLabel}
|
||||||
</AnimatePresence>
|
/>
|
||||||
|
{imageDTO && (
|
||||||
|
<IAIDraggable
|
||||||
|
data={draggableData}
|
||||||
|
disabled={isDragDisabled || !imageDTO}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{onClickReset && withResetIcon && imageDTO && (
|
||||||
|
<IAIIconButton
|
||||||
|
onClick={onClickReset}
|
||||||
|
aria-label={resetTooltip}
|
||||||
|
tooltip={resetTooltip}
|
||||||
|
icon={resetIcon}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 1,
|
||||||
|
insetInlineEnd: 1,
|
||||||
|
p: 0,
|
||||||
|
minW: 0,
|
||||||
|
svg: {
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
fill: 'base.100',
|
||||||
|
_hover: { fill: 'base.50' },
|
||||||
|
filter: resetIconShadow,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
40
invokeai/frontend/web/src/common/components/IAIDraggable.tsx
Normal file
40
invokeai/frontend/web/src/common/components/IAIDraggable.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
TypesafeDraggableData,
|
||||||
|
useDraggable,
|
||||||
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { MouseEvent, memo, useRef } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
type IAIDraggableProps = {
|
||||||
|
disabled?: boolean;
|
||||||
|
data?: TypesafeDraggableData;
|
||||||
|
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIDraggable = (props: IAIDraggableProps) => {
|
||||||
|
const { data, disabled, onClick } = props;
|
||||||
|
const dndId = useRef(uuidv4());
|
||||||
|
|
||||||
|
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||||
|
id: dndId.current,
|
||||||
|
disabled,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={onClick}
|
||||||
|
ref={setNodeRef}
|
||||||
|
position="absolute"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(IAIDraggable);
|
47
invokeai/frontend/web/src/common/components/IAIDroppable.tsx
Normal file
47
invokeai/frontend/web/src/common/components/IAIDroppable.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
TypesafeDroppableData,
|
||||||
|
isValidDrop,
|
||||||
|
useDroppable,
|
||||||
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { memo, useRef } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import IAIDropOverlay from './IAIDropOverlay';
|
||||||
|
|
||||||
|
type IAIDroppableProps = {
|
||||||
|
dropLabel?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
data?: TypesafeDroppableData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIDroppable = (props: IAIDroppableProps) => {
|
||||||
|
const { dropLabel, data, disabled } = props;
|
||||||
|
const dndId = useRef(uuidv4());
|
||||||
|
|
||||||
|
const { isOver, setNodeRef, active } = useDroppable({
|
||||||
|
id: dndId.current,
|
||||||
|
disabled,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={setNodeRef}
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isValidDrop(data, active) && (
|
||||||
|
<IAIDropOverlay isOver={isOver} label={dropLabel} />
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(IAIDroppable);
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Box, Flex, Icon } from '@chakra-ui/react';
|
||||||
|
import { FaExclamation } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const IAIErrorLoadingImageFallback = () => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
height: 'full',
|
||||||
|
width: 'full',
|
||||||
|
'::before': {
|
||||||
|
content: "''",
|
||||||
|
display: 'block',
|
||||||
|
pt: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
height: 'full',
|
||||||
|
width: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
bg: 'base.100',
|
||||||
|
color: 'base.500',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.700',
|
||||||
|
bg: 'base.850',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon as={FaExclamation} boxSize={16} opacity={0.7} />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAIErrorLoadingImageFallback;
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Box, Skeleton } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const IAIFillSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
height: 'full',
|
||||||
|
width: 'full',
|
||||||
|
'::before': {
|
||||||
|
content: "''",
|
||||||
|
display: 'block',
|
||||||
|
pt: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
height: 'full',
|
||||||
|
width: 'full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAIFillSkeleton;
|
@ -3,36 +3,22 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import {
|
import { modelsApi } from '../../services/api/endpoints/models';
|
||||||
modelsApi,
|
|
||||||
useGetMainModelsQuery,
|
|
||||||
} from '../../services/api/endpoints/models';
|
|
||||||
|
|
||||||
const readinessSelector = createSelector(
|
const readinessSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
(state, activeTabName) => {
|
(state, activeTabName) => {
|
||||||
const { generation, system, batch } = state;
|
const { generation, system } = state;
|
||||||
const { shouldGenerateVariations, seedWeights, initialImage, seed } =
|
const { shouldGenerateVariations, seedWeights, initialImage, seed } =
|
||||||
generation;
|
generation;
|
||||||
|
|
||||||
const { isProcessing, isConnected } = system;
|
const { isProcessing, isConnected } = system;
|
||||||
const {
|
|
||||||
isEnabled: isBatchEnabled,
|
|
||||||
asInitialImage,
|
|
||||||
imageNames: batchImageNames,
|
|
||||||
} = batch;
|
|
||||||
|
|
||||||
let isReady = true;
|
let isReady = true;
|
||||||
const reasonsWhyNotReady: string[] = [];
|
const reasonsWhyNotReady: string[] = [];
|
||||||
|
|
||||||
if (
|
if (activeTabName === 'img2img' && !initialImage) {
|
||||||
activeTabName === 'img2img' &&
|
|
||||||
!initialImage &&
|
|
||||||
!(asInitialImage && batchImageNames.length > 1)
|
|
||||||
) {
|
|
||||||
isReady = false;
|
isReady = false;
|
||||||
reasonsWhyNotReady.push('No initial image selected');
|
reasonsWhyNotReady.push('No initial image selected');
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
import {
|
|
||||||
Flex,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Heading,
|
|
||||||
Spacer,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
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 IAISwitch from 'common/components/IAISwitch';
|
|
||||||
import { ControlNetConfig } from 'features/controlNet/store/controlNetSlice';
|
|
||||||
import { ChangeEvent, memo, useCallback } from 'react';
|
|
||||||
import { controlNetToggled } from '../store/batchSlice';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
controlNet: ControlNetConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector, (state, controlNetId: string) => controlNetId],
|
|
||||||
(state, controlNetId) => {
|
|
||||||
const isControlNetEnabled = state.batch.controlNets.includes(controlNetId);
|
|
||||||
return { isControlNetEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const BatchControlNet = (props: Props) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { isControlNetEnabled } = useAppSelector((state) =>
|
|
||||||
selector(state, props.controlNet.controlNetId)
|
|
||||||
);
|
|
||||||
const { processorType, model } = props.controlNet;
|
|
||||||
|
|
||||||
const handleChangeAsControlNet = useCallback(() => {
|
|
||||||
dispatch(controlNetToggled(props.controlNet.controlNetId));
|
|
||||||
}, [dispatch, props.controlNet.controlNetId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
layerStyle="second"
|
|
||||||
sx={{ flexDir: 'column', gap: 1, p: 4, borderRadius: 'base' }}
|
|
||||||
>
|
|
||||||
<Flex sx={{ justifyContent: 'space-between' }}>
|
|
||||||
<FormControl as={Flex} onClick={handleChangeAsControlNet}>
|
|
||||||
<FormLabel>
|
|
||||||
<Heading size="sm">ControlNet</Heading>
|
|
||||||
</FormLabel>
|
|
||||||
<Spacer />
|
|
||||||
<Switch isChecked={isControlNetEnabled} />
|
|
||||||
</FormControl>
|
|
||||||
</Flex>
|
|
||||||
<Text>
|
|
||||||
<strong>Model:</strong> {model}
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<strong>Processor:</strong> {processorType}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BatchControlNet);
|
|
@ -1,116 +0,0 @@
|
|||||||
import { Box, Icon, Skeleton } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
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 {
|
|
||||||
batchImageRangeEndSelected,
|
|
||||||
batchImageSelected,
|
|
||||||
batchImageSelectionToggled,
|
|
||||||
imageRemovedFromBatch,
|
|
||||||
} from 'features/batch/store/batchSlice';
|
|
||||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
|
||||||
import { FaExclamationCircle } from 'react-icons/fa';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
|
|
||||||
const makeSelector = (image_name: string) =>
|
|
||||||
createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
(state) => ({
|
|
||||||
selectionCount: state.batch.selection.length,
|
|
||||||
isSelected: state.batch.selection.includes(image_name),
|
|
||||||
}),
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
type BatchImageProps = {
|
|
||||||
imageName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BatchImage = (props: BatchImageProps) => {
|
|
||||||
const {
|
|
||||||
currentData: imageDTO,
|
|
||||||
isFetching,
|
|
||||||
isError,
|
|
||||||
isSuccess,
|
|
||||||
} = useGetImageDTOQuery(props.imageName);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() => makeSelector(props.imageName),
|
|
||||||
[props.imageName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isSelected, selectionCount } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleClickRemove = useCallback(() => {
|
|
||||||
dispatch(imageRemovedFromBatch(props.imageName));
|
|
||||||
}, [dispatch, props.imageName]);
|
|
||||||
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
dispatch(batchImageRangeEndSelected(props.imageName));
|
|
||||||
} else if (e.ctrlKey || e.metaKey) {
|
|
||||||
dispatch(batchImageSelectionToggled(props.imageName));
|
|
||||||
} else {
|
|
||||||
dispatch(batchImageSelected(props.imageName));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, props.imageName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
|
||||||
if (selectionCount > 1) {
|
|
||||||
return {
|
|
||||||
id: 'batch',
|
|
||||||
payloadType: 'BATCH_SELECTION',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageDTO) {
|
|
||||||
return {
|
|
||||||
id: 'batch',
|
|
||||||
payloadType: 'IMAGE_DTO',
|
|
||||||
payload: { imageDTO },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [imageDTO, selectionCount]);
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return <Icon as={FaExclamationCircle} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFetching) {
|
|
||||||
return (
|
|
||||||
<Skeleton>
|
|
||||||
<Box w="full" h="full" aspectRatio="1/1" />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ position: 'relative', aspectRatio: '1/1' }}>
|
|
||||||
<IAIDndImage
|
|
||||||
imageDTO={imageDTO}
|
|
||||||
draggableData={draggableData}
|
|
||||||
isDropDisabled={true}
|
|
||||||
isUploadDisabled={true}
|
|
||||||
imageSx={{
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onClickReset={handleClickRemove}
|
|
||||||
resetTooltip="Remove from batch"
|
|
||||||
withResetIcon
|
|
||||||
thumbnail
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BatchImage);
|
|
@ -1,31 +0,0 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import BatchImageGrid from './BatchImageGrid';
|
|
||||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
|
||||||
import {
|
|
||||||
AddToBatchDropData,
|
|
||||||
isValidDrop,
|
|
||||||
useDroppable,
|
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
|
|
||||||
const droppableData: AddToBatchDropData = {
|
|
||||||
id: 'batch',
|
|
||||||
actionType: 'ADD_TO_BATCH',
|
|
||||||
};
|
|
||||||
|
|
||||||
const BatchImageContainer = () => {
|
|
||||||
const { isOver, setNodeRef, active } = useDroppable({
|
|
||||||
id: 'batch-manager',
|
|
||||||
data: droppableData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box ref={setNodeRef} position="relative" w="full" h="full">
|
|
||||||
<BatchImageGrid />
|
|
||||||
{isValidDrop(droppableData, active) && (
|
|
||||||
<IAIDropOverlay isOver={isOver} label="Add to Batch" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BatchImageContainer;
|
|
@ -1,54 +0,0 @@
|
|||||||
import { FaImages } from 'react-icons/fa';
|
|
||||||
import { Grid, GridItem } 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 BatchImage from './BatchImage';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
stateSelector,
|
|
||||||
(state) => {
|
|
||||||
const imageNames = state.batch.imageNames.concat().reverse();
|
|
||||||
|
|
||||||
return { imageNames };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const BatchImageGrid = () => {
|
|
||||||
const { imageNames } = useAppSelector(selector);
|
|
||||||
|
|
||||||
if (imageNames.length === 0) {
|
|
||||||
return (
|
|
||||||
<IAINoContentFallback
|
|
||||||
icon={FaImages}
|
|
||||||
boxSize={16}
|
|
||||||
label="No images in Batch"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
w: 'full',
|
|
||||||
minH: 0,
|
|
||||||
maxH: 'full',
|
|
||||||
overflowY: 'scroll',
|
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(128px, 1fr))`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{imageNames.map((imageName) => (
|
|
||||||
<GridItem key={imageName} sx={{ p: 1.5 }}>
|
|
||||||
<BatchImage imageName={imageName} />
|
|
||||||
</GridItem>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BatchImageGrid;
|
|
@ -1,103 +0,0 @@
|
|||||||
import { Flex, Heading, Spacer } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import IAISwitch from 'common/components/IAISwitch';
|
|
||||||
import {
|
|
||||||
asInitialImageToggled,
|
|
||||||
batchReset,
|
|
||||||
isEnabledChanged,
|
|
||||||
} from 'features/batch/store/batchSlice';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIButton from 'common/components/IAIButton';
|
|
||||||
import BatchImageContainer from './BatchImageGrid';
|
|
||||||
import { map } from 'lodash-es';
|
|
||||||
import BatchControlNet from './BatchControlNet';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
stateSelector,
|
|
||||||
(state) => {
|
|
||||||
const { controlNets } = state.controlNet;
|
|
||||||
const {
|
|
||||||
imageNames,
|
|
||||||
asInitialImage,
|
|
||||||
controlNets: batchControlNets,
|
|
||||||
isEnabled,
|
|
||||||
} = state.batch;
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageCount: imageNames.length,
|
|
||||||
asInitialImage,
|
|
||||||
controlNets,
|
|
||||||
batchControlNets,
|
|
||||||
isEnabled,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const BatchManager = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { imageCount, isEnabled, controlNets, batchControlNets } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleResetBatch = useCallback(() => {
|
|
||||||
dispatch(batchReset());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
|
||||||
dispatch(isEnabledChanged(!isEnabled));
|
|
||||||
}, [dispatch, isEnabled]);
|
|
||||||
|
|
||||||
const handleChangeAsInitialImage = useCallback(() => {
|
|
||||||
dispatch(asInitialImageToggled());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
flexDir: 'column',
|
|
||||||
position: 'relative',
|
|
||||||
gap: 2,
|
|
||||||
minW: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex sx={{ alignItems: 'center' }}>
|
|
||||||
<Heading
|
|
||||||
size={'md'}
|
|
||||||
sx={{ color: 'base.800', _dark: { color: 'base.200' } }}
|
|
||||||
>
|
|
||||||
{imageCount || 'No'} images
|
|
||||||
</Heading>
|
|
||||||
<Spacer />
|
|
||||||
<IAIButton onClick={handleResetBatch}>Reset</IAIButton>
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDir: 'column',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAISwitch
|
|
||||||
label="Use as Initial Image"
|
|
||||||
onChange={handleChangeAsInitialImage}
|
|
||||||
/>
|
|
||||||
{map(controlNets, (controlNet) => {
|
|
||||||
return (
|
|
||||||
<BatchControlNet
|
|
||||||
key={controlNet.controlNetId}
|
|
||||||
controlNet={controlNet}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
<BatchImageContainer />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BatchManager;
|
|
@ -1,142 +0,0 @@
|
|||||||
import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit';
|
|
||||||
import { uniq } from 'lodash-es';
|
|
||||||
import { imageDeleted } from 'services/api/thunks/image';
|
|
||||||
|
|
||||||
type BatchState = {
|
|
||||||
isEnabled: boolean;
|
|
||||||
imageNames: string[];
|
|
||||||
asInitialImage: boolean;
|
|
||||||
controlNets: string[];
|
|
||||||
selection: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialBatchState: BatchState = {
|
|
||||||
isEnabled: false,
|
|
||||||
imageNames: [],
|
|
||||||
asInitialImage: false,
|
|
||||||
controlNets: [],
|
|
||||||
selection: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const batch = createSlice({
|
|
||||||
name: 'batch',
|
|
||||||
initialState: initialBatchState,
|
|
||||||
reducers: {
|
|
||||||
isEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.isEnabled = action.payload;
|
|
||||||
},
|
|
||||||
imageAddedToBatch: (state, action: PayloadAction<string>) => {
|
|
||||||
state.imageNames = uniq(state.imageNames.concat(action.payload));
|
|
||||||
},
|
|
||||||
imagesAddedToBatch: (state, action: PayloadAction<string[]>) => {
|
|
||||||
state.imageNames = uniq(state.imageNames.concat(action.payload));
|
|
||||||
},
|
|
||||||
imageRemovedFromBatch: (state, action: PayloadAction<string>) => {
|
|
||||||
state.imageNames = state.imageNames.filter(
|
|
||||||
(imageName) => action.payload !== imageName
|
|
||||||
);
|
|
||||||
state.selection = state.selection.filter(
|
|
||||||
(imageName) => action.payload !== imageName
|
|
||||||
);
|
|
||||||
},
|
|
||||||
imagesRemovedFromBatch: (state, action: PayloadAction<string[]>) => {
|
|
||||||
state.imageNames = state.imageNames.filter(
|
|
||||||
(imageName) => !action.payload.includes(imageName)
|
|
||||||
);
|
|
||||||
state.selection = state.selection.filter(
|
|
||||||
(imageName) => !action.payload.includes(imageName)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
batchImageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
|
||||||
const rangeEndImageName = action.payload;
|
|
||||||
const lastSelectedImage = state.selection[state.selection.length - 1];
|
|
||||||
const lastClickedIndex = state.imageNames.findIndex(
|
|
||||||
(n) => n === lastSelectedImage
|
|
||||||
);
|
|
||||||
const currentClickedIndex = state.imageNames.findIndex(
|
|
||||||
(n) => n === rangeEndImageName
|
|
||||||
);
|
|
||||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
|
||||||
// We have a valid range!
|
|
||||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
|
||||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
|
||||||
|
|
||||||
const imagesToSelect = state.imageNames.slice(start, end + 1);
|
|
||||||
state.selection = uniq(state.selection.concat(imagesToSelect));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
batchImageSelectionToggled: (state, action: PayloadAction<string>) => {
|
|
||||||
if (
|
|
||||||
state.selection.includes(action.payload) &&
|
|
||||||
state.selection.length > 1
|
|
||||||
) {
|
|
||||||
state.selection = state.selection.filter(
|
|
||||||
(imageName) => imageName !== action.payload
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
state.selection = uniq(state.selection.concat(action.payload));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
batchImageSelected: (state, action: PayloadAction<string | null>) => {
|
|
||||||
state.selection = action.payload
|
|
||||||
? [action.payload]
|
|
||||||
: [String(state.imageNames[0])];
|
|
||||||
},
|
|
||||||
batchReset: (state) => {
|
|
||||||
state.imageNames = [];
|
|
||||||
state.selection = [];
|
|
||||||
},
|
|
||||||
asInitialImageToggled: (state) => {
|
|
||||||
state.asInitialImage = !state.asInitialImage;
|
|
||||||
},
|
|
||||||
controlNetAddedToBatch: (state, action: PayloadAction<string>) => {
|
|
||||||
state.controlNets = uniq(state.controlNets.concat(action.payload));
|
|
||||||
},
|
|
||||||
controlNetRemovedFromBatch: (state, action: PayloadAction<string>) => {
|
|
||||||
state.controlNets = state.controlNets.filter(
|
|
||||||
(controlNetId) => controlNetId !== action.payload
|
|
||||||
);
|
|
||||||
},
|
|
||||||
controlNetToggled: (state, action: PayloadAction<string>) => {
|
|
||||||
if (state.controlNets.includes(action.payload)) {
|
|
||||||
state.controlNets = state.controlNets.filter(
|
|
||||||
(controlNetId) => controlNetId !== action.payload
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
state.controlNets = uniq(state.controlNets.concat(action.payload));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder.addCase(imageDeleted.fulfilled, (state, action) => {
|
|
||||||
state.imageNames = state.imageNames.filter(
|
|
||||||
(imageName) => imageName !== action.meta.arg.image_name
|
|
||||||
);
|
|
||||||
state.selection = state.selection.filter(
|
|
||||||
(imageName) => imageName !== action.meta.arg.image_name
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
isEnabledChanged,
|
|
||||||
imageAddedToBatch,
|
|
||||||
imagesAddedToBatch,
|
|
||||||
imageRemovedFromBatch,
|
|
||||||
imagesRemovedFromBatch,
|
|
||||||
asInitialImageToggled,
|
|
||||||
controlNetAddedToBatch,
|
|
||||||
controlNetRemovedFromBatch,
|
|
||||||
batchReset,
|
|
||||||
controlNetToggled,
|
|
||||||
batchImageRangeEndSelected,
|
|
||||||
batchImageSelectionToggled,
|
|
||||||
batchImageSelected,
|
|
||||||
} = batch.actions;
|
|
||||||
|
|
||||||
export default batch.reducer;
|
|
||||||
|
|
||||||
export const selectionAddedToBatch = createAction(
|
|
||||||
'batch/selectionAddedToBatch'
|
|
||||||
);
|
|
@ -1,93 +0,0 @@
|
|||||||
import { Flex, useColorMode } from '@chakra-ui/react';
|
|
||||||
import { FaImages } from 'react-icons/fa';
|
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
import {
|
|
||||||
MoveBoardDropData,
|
|
||||||
isValidDrop,
|
|
||||||
useDroppable,
|
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
|
|
||||||
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
|
|
||||||
const handleAllImagesBoardClick = () => {
|
|
||||||
dispatch(boardIdSelected());
|
|
||||||
};
|
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = {
|
|
||||||
id: 'all-images-board',
|
|
||||||
actionType: 'MOVE_BOARD',
|
|
||||||
context: { boardId: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isOver, setNodeRef, active } = useDroppable({
|
|
||||||
id: `board_droppable_all_images`,
|
|
||||||
data: droppableData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
flexDir: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
borderRadius: 'base',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
ref={setNodeRef}
|
|
||||||
onClick={handleAllImagesBoardClick}
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 'base',
|
|
||||||
w: 'full',
|
|
||||||
aspectRatio: '1/1',
|
|
||||||
overflow: 'hidden',
|
|
||||||
shadow: isSelected ? 'selected.light' : undefined,
|
|
||||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAINoContentFallback
|
|
||||||
boxSize={8}
|
|
||||||
icon={FaImages}
|
|
||||||
sx={{
|
|
||||||
border: '2px solid var(--invokeai-colors-base-200)',
|
|
||||||
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AnimatePresence>
|
|
||||||
{isValidDrop(droppableData, active) && (
|
|
||||||
<IAIDropOverlay isOver={isOver} />
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
h: 'full',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: isSelected
|
|
||||||
? mode('base.900', 'base.50')(colorMode)
|
|
||||||
: mode('base.700', 'base.200')(colorMode),
|
|
||||||
fontWeight: isSelected ? 600 : undefined,
|
|
||||||
fontSize: 'xs',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
All Images
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AllImagesBoard;
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { FaImages } from 'react-icons/fa';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import GenericBoard from './GenericBoard';
|
||||||
|
|
||||||
|
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleAllImagesBoardClick = () => {
|
||||||
|
dispatch(boardIdSelected('all'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const droppableData: MoveBoardDropData = {
|
||||||
|
id: 'all-images-board',
|
||||||
|
actionType: 'MOVE_BOARD',
|
||||||
|
context: { boardId: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericBoard
|
||||||
|
droppableData={droppableData}
|
||||||
|
onClick={handleAllImagesBoardClick}
|
||||||
|
isSelected={isSelected}
|
||||||
|
icon={FaImages}
|
||||||
|
label="All Images"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AllImagesBoard;
|
@ -0,0 +1,42 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { AddToBatchDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { FaLayerGroup } from 'react-icons/fa';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import GenericBoard from './GenericBoard';
|
||||||
|
|
||||||
|
const selector = createSelector(stateSelector, (state) => {
|
||||||
|
return {
|
||||||
|
count: state.gallery.batchImageNames.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { count } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleBatchBoardClick = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected('batch'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const droppableData: AddToBatchDropData = {
|
||||||
|
id: 'batch-board',
|
||||||
|
actionType: 'ADD_TO_BATCH',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericBoard
|
||||||
|
droppableData={droppableData}
|
||||||
|
onClick={handleBatchBoardClick}
|
||||||
|
isSelected={isSelected}
|
||||||
|
icon={FaLayerGroup}
|
||||||
|
label="Batch"
|
||||||
|
badgeCount={count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BatchBoard;
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CloseIcon } from '@chakra-ui/icons';
|
||||||
import {
|
import {
|
||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
Flex,
|
||||||
@ -9,17 +10,18 @@ import {
|
|||||||
InputRightElement,
|
InputRightElement,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
||||||
import { memo, useState } from 'react';
|
|
||||||
import HoverableBoard from './HoverableBoard';
|
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
import AddBoardButton from './AddBoardButton';
|
import AddBoardButton from './AddBoardButton';
|
||||||
import AllImagesBoard from './AllImagesBoard';
|
import AllImagesBoard from './AllImagesBoard';
|
||||||
import { CloseIcon } from '@chakra-ui/icons';
|
import BatchBoard from './BatchBoard';
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
import GalleryBoard from './GalleryBoard';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -42,6 +44,8 @@ const BoardsList = (props: Props) => {
|
|||||||
|
|
||||||
const { data: boards } = useListAllBoardsQuery();
|
const { data: boards } = useListAllBoardsQuery();
|
||||||
|
|
||||||
|
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
|
||||||
|
|
||||||
const filteredBoards = searchText
|
const filteredBoards = searchText
|
||||||
? boards?.filter((board) =>
|
? boards?.filter((board) =>
|
||||||
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
||||||
@ -115,14 +119,21 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!searchMode && (
|
{!searchMode && (
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
<>
|
||||||
<AllImagesBoard isSelected={!selectedBoardId} />
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
</GridItem>
|
<AllImagesBoard isSelected={selectedBoardId === 'all'} />
|
||||||
|
</GridItem>
|
||||||
|
{isBatchEnabled && (
|
||||||
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
|
<BatchBoard isSelected={selectedBoardId === 'batch'} />
|
||||||
|
</GridItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||||
<HoverableBoard
|
<GalleryBoard
|
||||||
board={board}
|
board={board}
|
||||||
isSelected={selectedBoardId === board.board_id}
|
isSelected={selectedBoardId === board.board_id}
|
||||||
/>
|
/>
|
@ -12,35 +12,31 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { memo, useCallback, useContext } from 'react';
|
|
||||||
import { FaFolder, FaTrash } from 'react-icons/fa';
|
|
||||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||||
import { BoardDTO } from 'services/api/types';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||||
|
import { FaFolder, FaImages, FaTrash } from 'react-icons/fa';
|
||||||
import {
|
import {
|
||||||
useDeleteBoardMutation,
|
useDeleteBoardMutation,
|
||||||
useUpdateBoardMutation,
|
useUpdateBoardMutation,
|
||||||
} from 'services/api/endpoints/boards';
|
} from 'services/api/endpoints/boards';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
// import { boardAddedToBatch } from 'app/store/middleware/listenerMiddleware/listeners/addBoardToBatch';
|
||||||
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import {
|
import { DeleteBoardImagesContext } from '../../../../../app/contexts/DeleteBoardImagesContext';
|
||||||
MoveBoardDropData,
|
|
||||||
isValidDrop,
|
|
||||||
useDroppable,
|
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
|
|
||||||
interface HoverableBoardProps {
|
interface GalleryBoardProps {
|
||||||
board: BoardDTO;
|
board: BoardDTO;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||||
@ -71,21 +67,22 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
deleteBoard(board_id);
|
deleteBoard(board_id);
|
||||||
}, [board_id, deleteBoard]);
|
}, [board_id, deleteBoard]);
|
||||||
|
|
||||||
|
const handleAddBoardToBatch = useCallback(() => {
|
||||||
|
// dispatch(boardAddedToBatch({ board_id }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeleteBoardAndImages = useCallback(() => {
|
const handleDeleteBoardAndImages = useCallback(() => {
|
||||||
console.log({ board });
|
|
||||||
onClickDeleteBoardImages(board);
|
onClickDeleteBoardImages(board);
|
||||||
}, [board, onClickDeleteBoardImages]);
|
}, [board, onClickDeleteBoardImages]);
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = {
|
const droppableData: MoveBoardDropData = useMemo(
|
||||||
id: board_id,
|
() => ({
|
||||||
actionType: 'MOVE_BOARD',
|
id: board_id,
|
||||||
context: { boardId: board_id },
|
actionType: 'MOVE_BOARD',
|
||||||
};
|
context: { boardId: board_id },
|
||||||
|
}),
|
||||||
const { isOver, setNodeRef, active } = useDroppable({
|
[board_id]
|
||||||
id: `board_droppable_${board_id}`,
|
);
|
||||||
data: droppableData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||||
@ -94,16 +91,25 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
renderMenu={() => (
|
renderMenu={() => (
|
||||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
<MenuList sx={{ visibility: 'visible !important' }}>
|
||||||
{board.image_count > 0 && (
|
{board.image_count > 0 && (
|
||||||
<MenuItem
|
<>
|
||||||
sx={{ color: 'error.300' }}
|
<MenuItem
|
||||||
icon={<FaTrash />}
|
isDisabled={!board.image_count}
|
||||||
onClickCapture={handleDeleteBoardAndImages}
|
icon={<FaImages />}
|
||||||
>
|
onClickCapture={handleAddBoardToBatch}
|
||||||
Delete Board and Images
|
>
|
||||||
</MenuItem>
|
Add Board to Batch
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClickCapture={handleDeleteBoardAndImages}
|
||||||
|
>
|
||||||
|
Delete Board and Images
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: mode('error.700', 'error.300')(colorMode) }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
onClickCapture={handleDeleteBoard}
|
onClickCapture={handleDeleteBoard}
|
||||||
>
|
>
|
||||||
@ -127,7 +133,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
ref={setNodeRef}
|
|
||||||
onClick={handleSelectBoard}
|
onClick={handleSelectBoard}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -167,11 +172,7 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
>
|
>
|
||||||
<Badge variant="solid">{board.image_count}</Badge>
|
<Badge variant="solid">{board.image_count}</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<AnimatePresence>
|
<IAIDroppable data={droppableData} />
|
||||||
{isValidDrop(droppableData, active) && (
|
|
||||||
<IAIDropOverlay isOver={isOver} />
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
@ -219,6 +220,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
HoverableBoard.displayName = 'HoverableBoard';
|
GalleryBoard.displayName = 'HoverableBoard';
|
||||||
|
|
||||||
export default HoverableBoard;
|
export default GalleryBoard;
|
@ -0,0 +1,83 @@
|
|||||||
|
import { As, Badge, Flex } from '@chakra-ui/react';
|
||||||
|
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
|
||||||
|
type GenericBoardProps = {
|
||||||
|
droppableData: TypesafeDroppableData;
|
||||||
|
onClick: () => void;
|
||||||
|
isSelected: boolean;
|
||||||
|
icon: As;
|
||||||
|
label: string;
|
||||||
|
badgeCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GenericBoard = (props: GenericBoardProps) => {
|
||||||
|
const { droppableData, onClick, isSelected, icon, label, badgeCount } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDir: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
borderRadius: 'base',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
w: 'full',
|
||||||
|
aspectRatio: '1/1',
|
||||||
|
overflow: 'hidden',
|
||||||
|
shadow: isSelected ? 'selected.light' : undefined,
|
||||||
|
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IAINoContentFallback
|
||||||
|
boxSize={8}
|
||||||
|
icon={icon}
|
||||||
|
sx={{
|
||||||
|
border: '2px solid var(--invokeai-colors-base-200)',
|
||||||
|
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
top: 0,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badgeCount !== undefined && (
|
||||||
|
<Badge variant="solid">{badgeCount}</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<IAIDroppable data={droppableData} />
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontWeight: isSelected ? 600 : undefined,
|
||||||
|
fontSize: 'xs',
|
||||||
|
color: isSelected ? 'base.900' : 'base.700',
|
||||||
|
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenericBoard;
|
@ -8,8 +8,6 @@ import IAIButton from 'common/components/IAIButton';
|
|||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import IAIPopover from 'common/components/IAIPopover';
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
|
||||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
|
||||||
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
@ -36,7 +34,6 @@ import {
|
|||||||
FaCode,
|
FaCode,
|
||||||
FaCopy,
|
FaCopy,
|
||||||
FaDownload,
|
FaDownload,
|
||||||
FaExpand,
|
|
||||||
FaExpandArrowsAlt,
|
FaExpandArrowsAlt,
|
||||||
FaGrinStars,
|
FaGrinStars,
|
||||||
FaHourglassHalf,
|
FaHourglassHalf,
|
||||||
@ -50,11 +47,11 @@ import {
|
|||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
({ gallery, system, postprocessing, ui, lightbox }, activeTabName) => {
|
({ gallery, system, postprocessing, ui }, activeTabName) => {
|
||||||
const {
|
const {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -66,8 +63,6 @@ const currentImageButtonsSelector = createSelector(
|
|||||||
|
|
||||||
const { upscalingLevel, facetoolStrength } = postprocessing;
|
const { upscalingLevel, facetoolStrength } = postprocessing;
|
||||||
|
|
||||||
const { isLightboxOpen } = lightbox;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
shouldHidePreview,
|
shouldHidePreview,
|
||||||
@ -88,7 +83,6 @@ const currentImageButtonsSelector = createSelector(
|
|||||||
shouldDisableToolbarButtons: Boolean(progressImage) || !lastSelectedImage,
|
shouldDisableToolbarButtons: Boolean(progressImage) || !lastSelectedImage,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
isLightboxOpen,
|
|
||||||
shouldHidePreview,
|
shouldHidePreview,
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
lastSelectedImage,
|
lastSelectedImage,
|
||||||
@ -114,14 +108,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
facetoolStrength,
|
facetoolStrength,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
isLightboxOpen,
|
|
||||||
activeTabName,
|
activeTabName,
|
||||||
shouldHidePreview,
|
|
||||||
lastSelectedImage,
|
lastSelectedImage,
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
} = useAppSelector(currentImageButtonsSelector);
|
} = useAppSelector(currentImageButtonsSelector);
|
||||||
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
|
||||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||||
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
|
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
|
||||||
@ -149,30 +140,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
const metadata = metadataData?.metadata;
|
const metadata = metadataData?.metadata;
|
||||||
|
|
||||||
// const handleCopyImage = useCallback(async () => {
|
|
||||||
// if (!image?.url) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const url = getUrl(image.url);
|
|
||||||
|
|
||||||
// if (!url) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const blob = await fetch(url).then((res) => res.blob());
|
|
||||||
// const data = [new ClipboardItem({ [blob.type]: blob })];
|
|
||||||
|
|
||||||
// await navigator.clipboard.write(data);
|
|
||||||
|
|
||||||
// toast({
|
|
||||||
// title: t('toast.imageCopied'),
|
|
||||||
// status: 'success',
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// });
|
|
||||||
// }, [getUrl, t, image?.url, toast]);
|
|
||||||
|
|
||||||
const handleCopyImageLink = useCallback(() => {
|
const handleCopyImageLink = useCallback(() => {
|
||||||
const getImageUrl = () => {
|
const getImageUrl = () => {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
@ -318,7 +285,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
dispatch(sentImageToCanvas());
|
dispatch(sentImageToCanvas());
|
||||||
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
|
|
||||||
|
|
||||||
dispatch(setInitialCanvasImage(image));
|
dispatch(setInitialCanvasImage(image));
|
||||||
dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
@ -333,7 +299,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
duration: 2500,
|
duration: 2500,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
}, [image, isLightboxOpen, dispatch, activeTabName, toaster, t]);
|
}, [image, dispatch, activeTabName, toaster, t]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'i',
|
'i',
|
||||||
@ -356,10 +322,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||||
}, [dispatch, shouldShowProgressInViewer]);
|
}, [dispatch, shouldShowProgressInViewer]);
|
||||||
|
|
||||||
const handleLightBox = useCallback(() => {
|
|
||||||
dispatch(setIsLightboxOpen(!isLightboxOpen));
|
|
||||||
}, [dispatch, isLightboxOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
@ -429,24 +391,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</IAIPopover>
|
</IAIPopover>
|
||||||
{isLightboxEnabled && (
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<FaExpand />}
|
|
||||||
tooltip={
|
|
||||||
!isLightboxOpen
|
|
||||||
? `${t('parameters.openInViewer')} (Z)`
|
|
||||||
: `${t('parameters.closeViewer')} (Z)`
|
|
||||||
}
|
|
||||||
aria-label={
|
|
||||||
!isLightboxOpen
|
|
||||||
? `${t('parameters.openInViewer')} (Z)`
|
|
||||||
: `${t('parameters.closeViewer')} (Z)`
|
|
||||||
}
|
|
||||||
isChecked={isLightboxOpen}
|
|
||||||
onClick={handleLightBox}
|
|
||||||
isDisabled={shouldDisableToolbarButtons}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
@ -8,14 +8,15 @@ import {
|
|||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
|
import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage';
|
||||||
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { useNextPrevImage } from '../hooks/useNextPrevImage';
|
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
import NextPrevImageButtons from '../NextPrevImageButtons';
|
||||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
|
||||||
|
|
||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
[stateSelector, selectLastSelectedImage],
|
[stateSelector, selectLastSelectedImage],
|
||||||
@ -115,8 +116,27 @@ const CurrentImagePreview = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show and hide the next/prev buttons on mouse move
|
||||||
|
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
const timeoutId = useRef(0);
|
||||||
|
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setShouldShowNextPrevButtons(true);
|
||||||
|
window.clearTimeout(timeoutId.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
timeoutId.current = window.setTimeout(() => {
|
||||||
|
setShouldShowNextPrevButtons(false);
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
sx={{
|
sx={{
|
||||||
width: 'full',
|
width: 'full',
|
||||||
height: 'full',
|
height: 'full',
|
||||||
@ -164,19 +184,33 @@ const CurrentImagePreview = () => {
|
|||||||
<ImageMetadataViewer image={imageDTO} />
|
<ImageMetadataViewer image={imageDTO} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{!shouldShowImageDetails && imageDTO && (
|
<AnimatePresence>
|
||||||
<Box
|
{!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && (
|
||||||
sx={{
|
<motion.div
|
||||||
position: 'absolute',
|
key="nextPrevButtons"
|
||||||
top: '0',
|
initial={{
|
||||||
width: 'full',
|
opacity: 0,
|
||||||
height: 'full',
|
}}
|
||||||
pointerEvents: 'none',
|
animate={{
|
||||||
}}
|
opacity: 1,
|
||||||
>
|
transition: { duration: 0.1 },
|
||||||
<NextPrevImageButtons />
|
}}
|
||||||
</Box>
|
exit={{
|
||||||
)}
|
opacity: 0,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NextPrevImageButtons />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -5,32 +5,23 @@ import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice
|
|||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import './ImageGallery.css';
|
|
||||||
import ImageGalleryContent from './ImageGalleryContent';
|
|
||||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
|
||||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
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 {
|
import {
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
uiSelector,
|
uiSelector,
|
||||||
} from 'features/ui/store/uiSelectors';
|
} from 'features/ui/store/uiSelectors';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import ImageGalleryContent from './ImageGalleryContent';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[
|
[activeTabNameSelector, uiSelector, gallerySelector, isStagingSelector],
|
||||||
activeTabNameSelector,
|
(activeTabName, ui, gallery, isStaging) => {
|
||||||
uiSelector,
|
|
||||||
gallerySelector,
|
|
||||||
isStagingSelector,
|
|
||||||
lightboxSelector,
|
|
||||||
],
|
|
||||||
(activeTabName, ui, gallery, isStaging, lightbox) => {
|
|
||||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
const { shouldPinGallery, shouldShowGallery } = ui;
|
||||||
const { galleryImageMinimumWidth } = gallery;
|
const { galleryImageMinimumWidth } = gallery;
|
||||||
const { isLightboxOpen } = lightbox;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTabName,
|
activeTabName,
|
||||||
@ -39,7 +30,6 @@ const selector = createSelector(
|
|||||||
shouldShowGallery,
|
shouldShowGallery,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
isResizable: activeTabName !== 'unifiedCanvas',
|
isResizable: activeTabName !== 'unifiedCanvas',
|
||||||
isLightboxOpen,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58,7 +48,6 @@ const GalleryDrawer = () => {
|
|||||||
// activeTabName,
|
// activeTabName,
|
||||||
// isStaging,
|
// isStaging,
|
||||||
// isResizable,
|
// isResizable,
|
||||||
// isLightboxOpen,
|
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const handleCloseGallery = () => {
|
const handleCloseGallery = () => {
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { MenuList } 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 { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
||||||
|
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
imageDTO: ImageDTO;
|
||||||
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
({ gallery }) => {
|
||||||
|
const selectionCount = gallery.selection.length;
|
||||||
|
|
||||||
|
return { selectionCount };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { selectionCount } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu<HTMLDivElement>
|
||||||
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
|
renderMenu={() => (
|
||||||
|
<MenuList sx={{ visibility: 'visible !important' }}>
|
||||||
|
{selectionCount === 1 ? (
|
||||||
|
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||||
|
) : (
|
||||||
|
<MultipleSelectionMenuItems />
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ImageContextMenu);
|
@ -0,0 +1,40 @@
|
|||||||
|
import { MenuItem } from '@chakra-ui/react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { FaFolder, FaFolderPlus, FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const MultipleSelectionMenuItems = () => {
|
||||||
|
const handleAddSelectionToBoard = useCallback(() => {
|
||||||
|
// TODO: add selection to board
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteSelection = useCallback(() => {
|
||||||
|
// TODO: delete all selected images
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddSelectionToBatch = useCallback(() => {
|
||||||
|
// TODO: add selection to batch
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem icon={<FaFolder />} onClickCapture={handleAddSelectionToBoard}>
|
||||||
|
Move Selection to Board
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<FaFolderPlus />}
|
||||||
|
onClickCapture={handleAddSelectionToBatch}
|
||||||
|
>
|
||||||
|
Add Selection to Batch
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClickCapture={handleDeleteSelection}
|
||||||
|
>
|
||||||
|
Delete Selection
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultipleSelectionMenuItems;
|
@ -1,16 +1,15 @@
|
|||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { MenuItem, MenuList } from '@chakra-ui/react';
|
import { MenuItem } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
|
||||||
import { imagesAddedToBatch } from 'features/batch/store/batchSlice';
|
|
||||||
import {
|
import {
|
||||||
resizeAndScaleCanvas,
|
resizeAndScaleCanvas,
|
||||||
setInitialCanvasImage,
|
setInitialCanvasImage,
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice';
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
@ -18,109 +17,35 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
|||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
|
import { FaFolder, FaShare, FaTrash } from 'react-icons/fa';
|
||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext';
|
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
|
||||||
type Props = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
image: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageContextMenu = ({ image, children }: Props) => {
|
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||||
|
const { imageDTO } = props;
|
||||||
|
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(
|
createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ gallery }) => {
|
({ gallery }) => {
|
||||||
const selectionCount = gallery.selection.length;
|
const isInBatch = gallery.batchImageNames.includes(
|
||||||
|
imageDTO.image_name
|
||||||
return { selectionCount };
|
);
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const { selectionCount } = useAppSelector(selector);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
|
||||||
if (!image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(imageToDeleteSelected(image));
|
|
||||||
}, [dispatch, image]);
|
|
||||||
|
|
||||||
const handleAddToBoard = useCallback(() => {
|
|
||||||
onClickAddToBoard(image);
|
|
||||||
}, [image, onClickAddToBoard]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenu<HTMLDivElement>
|
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
|
||||||
renderMenu={() => (
|
|
||||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
|
||||||
{selectionCount === 1 ? (
|
|
||||||
<SingleSelectionMenuItems image={image} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
isDisabled={true}
|
|
||||||
icon={<FaFolder />}
|
|
||||||
onClickCapture={handleAddToBoard}
|
|
||||||
>
|
|
||||||
Move Selection to Board
|
|
||||||
</MenuItem>
|
|
||||||
{/* <MenuItem
|
|
||||||
icon={<FaFolderPlus />}
|
|
||||||
onClickCapture={handleAddSelectionToBatch}
|
|
||||||
>
|
|
||||||
Add Selection to Batch
|
|
||||||
</MenuItem> */}
|
|
||||||
<MenuItem
|
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
|
||||||
icon={<FaTrash />}
|
|
||||||
onClickCapture={handleDelete}
|
|
||||||
>
|
|
||||||
Delete Selection
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MenuList>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ImageContextMenu);
|
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
|
||||||
image: ImageDTO;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|
||||||
const { image } = props;
|
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
({ batch }) => {
|
|
||||||
const isInBatch = batch.imageNames.includes(image.image_name);
|
|
||||||
|
|
||||||
return { isInBatch };
|
return { isInBatch };
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
),
|
),
|
||||||
[image.image_name]
|
[imageDTO.image_name]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isInBatch } = useAppSelector(selector);
|
const { isInBatch } = useAppSelector(selector);
|
||||||
@ -129,21 +54,21 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
|
||||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||||
|
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
|
||||||
|
|
||||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
||||||
|
|
||||||
const { currentData } = useGetImageMetadataQuery(image.image_name);
|
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
||||||
|
|
||||||
const metadata = currentData?.metadata;
|
const metadata = currentData?.metadata;
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!image) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(image));
|
dispatch(imageToDeleteSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
@ -161,12 +86,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(image));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const handleSendToCanvas = () => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
dispatch(sentImageToCanvas());
|
dispatch(sentImageToCanvas());
|
||||||
dispatch(setInitialCanvasImage(image));
|
dispatch(setInitialCanvasImage(imageDTO));
|
||||||
dispatch(resizeAndScaleCanvas());
|
dispatch(resizeAndScaleCanvas());
|
||||||
dispatch(setActiveTab('unifiedCanvas'));
|
dispatch(setActiveTab('unifiedCanvas'));
|
||||||
|
|
||||||
@ -176,47 +101,40 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
duration: 2500,
|
duration: 2500,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
};
|
}, [dispatch, imageDTO, t, toaster]);
|
||||||
|
|
||||||
const handleUseAllParameters = useCallback(() => {
|
const handleUseAllParameters = useCallback(() => {
|
||||||
console.log(metadata);
|
console.log(metadata);
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
}, [metadata, recallAllParameters]);
|
}, [metadata, recallAllParameters]);
|
||||||
|
|
||||||
const handleLightBox = () => {
|
|
||||||
// dispatch(setCurrentImage(image));
|
|
||||||
// dispatch(setIsLightboxOpen(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToBoard = useCallback(() => {
|
const handleAddToBoard = useCallback(() => {
|
||||||
onClickAddToBoard(image);
|
onClickAddToBoard(imageDTO);
|
||||||
}, [image, onClickAddToBoard]);
|
}, [imageDTO, onClickAddToBoard]);
|
||||||
|
|
||||||
const handleRemoveFromBoard = useCallback(() => {
|
const handleRemoveFromBoard = useCallback(() => {
|
||||||
if (!image.board_id) {
|
if (!imageDTO.board_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
|
removeFromBoard({
|
||||||
}, [image.board_id, image.image_name, removeFromBoard]);
|
board_id: imageDTO.board_id,
|
||||||
|
image_name: imageDTO.image_name,
|
||||||
|
});
|
||||||
|
}, [imageDTO.board_id, imageDTO.image_name, removeFromBoard]);
|
||||||
|
|
||||||
const handleOpenInNewTab = () => {
|
const handleOpenInNewTab = useCallback(() => {
|
||||||
window.open(image.image_url, '_blank');
|
window.open(imageDTO.image_url, '_blank');
|
||||||
};
|
}, [imageDTO.image_url]);
|
||||||
|
|
||||||
const handleAddToBatch = useCallback(() => {
|
const handleAddToBatch = useCallback(() => {
|
||||||
dispatch(imagesAddedToBatch([image.image_name]));
|
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
||||||
}, [dispatch, image.image_name]);
|
}, [dispatch, imageDTO.image_name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
||||||
{t('common.openInNewTab')}
|
{t('common.openInNewTab')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{isLightboxEnabled && (
|
|
||||||
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
|
|
||||||
{t('parameters.openInViewer')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
@ -258,17 +176,19 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
{t('parameters.sendToUnifiedCanvas')}
|
{t('parameters.sendToUnifiedCanvas')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
{isBatchEnabled && (
|
||||||
icon={<FaFolder />}
|
<MenuItem
|
||||||
isDisabled={isInBatch}
|
icon={<FaFolder />}
|
||||||
onClickCapture={handleAddToBatch}
|
isDisabled={isInBatch}
|
||||||
>
|
onClickCapture={handleAddToBatch}
|
||||||
Add to Batch
|
>
|
||||||
</MenuItem>
|
Add to Batch
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
||||||
{image.board_id ? 'Change Board' : 'Add to Board'}
|
{imageDTO.board_id ? 'Change Board' : 'Add to Board'}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{image.board_id && (
|
{imageDTO.board_id && (
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleRemoveFromBoard}>
|
<MenuItem icon={<FaFolder />} onClickCapture={handleRemoveFromBoard}>
|
||||||
Remove from Board
|
Remove from Board
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -283,3 +203,5 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default memo(SingleSelectionMenuItems);
|
@ -1,35 +0,0 @@
|
|||||||
.ltr-image-gallery-css-transition-enter {
|
|
||||||
transform: translateX(150%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ltr-image-gallery-css-transition-enter-active {
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: all 120ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ltr-image-gallery-css-transition-exit {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ltr-image-gallery-css-transition-exit-active {
|
|
||||||
transform: translateX(150%);
|
|
||||||
transition: all 120ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtl-image-gallery-css-transition-enter {
|
|
||||||
transform: translateX(-150%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtl-image-gallery-css-transition-enter-active {
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: all 120ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtl-image-gallery-css-transition-exit {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtl-image-gallery-css-transition-exit-active {
|
|
||||||
transform: translateX(-150%);
|
|
||||||
transition: all 120ms ease-out;
|
|
||||||
}
|
|
@ -19,7 +19,7 @@ import {
|
|||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
||||||
|
|
||||||
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
import { ChangeEvent, memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||||
import { FaImage, FaServer, FaWrench } from 'react-icons/fa';
|
import { FaImage, FaServer, FaWrench } from 'react-icons/fa';
|
||||||
@ -29,16 +29,12 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import {
|
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
||||||
ASSETS_CATEGORIES,
|
|
||||||
IMAGE_CATEGORIES,
|
|
||||||
imageCategoriesChanged,
|
|
||||||
shouldAutoSwitchChanged,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import BoardsList from './Boards/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
import ImageGalleryGrid from './ImageGalleryGrid';
|
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||||
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -66,6 +62,7 @@ const ImageGalleryContent = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
|
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
@ -83,6 +80,16 @@ const ImageGalleryContent = () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const boardTitle = useMemo(() => {
|
||||||
|
if (selectedBoardId === 'batch') {
|
||||||
|
return 'Batch';
|
||||||
|
}
|
||||||
|
if (selectedBoard) {
|
||||||
|
return selectedBoard.board_name;
|
||||||
|
}
|
||||||
|
return 'All Images';
|
||||||
|
}, [selectedBoard, selectedBoardId]);
|
||||||
|
|
||||||
const { isOpen: isBoardListOpen, onToggle } = useDisclosure();
|
const { isOpen: isBoardListOpen, onToggle } = useDisclosure();
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
@ -95,12 +102,10 @@ const ImageGalleryContent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickImagesCategory = useCallback(() => {
|
const handleClickImagesCategory = useCallback(() => {
|
||||||
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
|
|
||||||
dispatch(setGalleryView('images'));
|
dispatch(setGalleryView('images'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleClickAssetsCategory = useCallback(() => {
|
const handleClickAssetsCategory = useCallback(() => {
|
||||||
dispatch(imageCategoriesChanged(ASSETS_CATEGORIES));
|
|
||||||
dispatch(setGalleryView('assets'));
|
dispatch(setGalleryView('assets'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
@ -163,7 +168,7 @@ const ImageGalleryContent = () => {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedBoard ? selectedBoard.board_name : 'All Images'}
|
{boardTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronUpIcon
|
<ChevronUpIcon
|
||||||
sx={{
|
sx={{
|
||||||
@ -216,8 +221,12 @@ const ImageGalleryContent = () => {
|
|||||||
<BoardsList isOpen={isBoardListOpen} />
|
<BoardsList isOpen={isBoardListOpen} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex direction="column" gap={2} h="full" w="full">
|
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
||||||
<ImageGalleryGrid />
|
{selectedBoardId === 'batch' ? (
|
||||||
|
<BatchImageGrid />
|
||||||
|
) : (
|
||||||
|
<GalleryImageGrid />
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
@ -1,233 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
FlexProps,
|
|
||||||
Grid,
|
|
||||||
Skeleton,
|
|
||||||
Spinner,
|
|
||||||
forwardRef,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIButton from 'common/components/IAIButton';
|
|
||||||
import { IMAGE_LIMIT } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
PropsWithChildren,
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaImage } from 'react-icons/fa';
|
|
||||||
import GalleryImage from './GalleryImage';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState, stateSelector } from 'app/store/store';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|
||||||
import { selectFilteredImages } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { VirtuosoGrid } from 'react-virtuoso';
|
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
|
||||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector, selectFilteredImages],
|
|
||||||
(state, filteredImages) => {
|
|
||||||
const {
|
|
||||||
categories,
|
|
||||||
total: allImagesTotal,
|
|
||||||
isLoading,
|
|
||||||
isFetching,
|
|
||||||
selectedBoardId,
|
|
||||||
} = state.gallery;
|
|
||||||
|
|
||||||
let images = filteredImages as (ImageDTO | 'loading')[];
|
|
||||||
|
|
||||||
if (!isLoading && isFetching) {
|
|
||||||
// loading, not not the initial load
|
|
||||||
images = images.concat(Array(IMAGE_LIMIT).fill('loading'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
images,
|
|
||||||
allImagesTotal,
|
|
||||||
isLoading,
|
|
||||||
isFetching,
|
|
||||||
categories,
|
|
||||||
selectedBoardId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const ImageGalleryGrid = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const rootRef = useRef(null);
|
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
|
||||||
defer: true,
|
|
||||||
options: {
|
|
||||||
scrollbars: {
|
|
||||||
visibility: 'auto',
|
|
||||||
autoHide: 'leave',
|
|
||||||
autoHideDelay: 1300,
|
|
||||||
theme: 'os-theme-dark',
|
|
||||||
},
|
|
||||||
overflow: { x: 'hidden' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
isLoading,
|
|
||||||
isFetching,
|
|
||||||
allImagesTotal,
|
|
||||||
categories,
|
|
||||||
selectedBoardId,
|
|
||||||
} = useAppSelector(selector);
|
|
||||||
|
|
||||||
const { selectedBoard } = useListAllBoardsQuery(undefined, {
|
|
||||||
selectFromResult: ({ data }) => ({
|
|
||||||
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredImagesTotal = useMemo(
|
|
||||||
() => selectedBoard?.image_count ?? allImagesTotal,
|
|
||||||
[allImagesTotal, selectedBoard?.image_count]
|
|
||||||
);
|
|
||||||
|
|
||||||
const areMoreAvailable = useMemo(() => {
|
|
||||||
return images.length < filteredImagesTotal;
|
|
||||||
}, [images.length, filteredImagesTotal]);
|
|
||||||
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
|
||||||
dispatch(
|
|
||||||
receivedPageOfImages({
|
|
||||||
categories,
|
|
||||||
board_id: selectedBoardId,
|
|
||||||
is_intermediate: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [categories, dispatch, selectedBoardId]);
|
|
||||||
|
|
||||||
const handleEndReached = useMemo(() => {
|
|
||||||
if (areMoreAvailable && !isLoading) {
|
|
||||||
return handleLoadMoreImages;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { current: root } = rootRef;
|
|
||||||
if (scroller && root) {
|
|
||||||
initialize({
|
|
||||||
target: root,
|
|
||||||
elements: {
|
|
||||||
viewport: scroller,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => osInstance()?.destroy();
|
|
||||||
}, [scroller, initialize, osInstance]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spinner
|
|
||||||
size="xl"
|
|
||||||
sx={{ color: 'base.300', _dark: { color: 'base.700' } }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (images.length) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
|
||||||
<VirtuosoGrid
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
data={images}
|
|
||||||
endReached={handleEndReached}
|
|
||||||
components={{
|
|
||||||
Item: ItemContainer,
|
|
||||||
List: ListContainer,
|
|
||||||
}}
|
|
||||||
scrollerRef={setScroller}
|
|
||||||
itemContent={(index, item) =>
|
|
||||||
typeof item === 'string' ? (
|
|
||||||
<Skeleton sx={{ w: 'full', h: 'full', aspectRatio: '1/1' }} />
|
|
||||||
) : (
|
|
||||||
<GalleryImage
|
|
||||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
|
||||||
imageDTO={item}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<IAIButton
|
|
||||||
onClick={handleLoadMoreImages}
|
|
||||||
isDisabled={!areMoreAvailable}
|
|
||||||
isLoading={isFetching}
|
|
||||||
loadingText="Loading"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
{areMoreAvailable
|
|
||||||
? t('gallery.loadMore')
|
|
||||||
: t('gallery.allImagesLoaded')}
|
|
||||||
</IAIButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IAINoContentFallback
|
|
||||||
label={t('gallery.noImagesInGallery')}
|
|
||||||
icon={FaImage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
|
||||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
|
||||||
<Box className="item-container" ref={ref} p={1.5}>
|
|
||||||
{props.children}
|
|
||||||
</Box>
|
|
||||||
));
|
|
||||||
|
|
||||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
|
||||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
|
||||||
const galleryImageMinimumWidth = useAppSelector(
|
|
||||||
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
{...props}
|
|
||||||
className="list-container"
|
|
||||||
ref={ref}
|
|
||||||
sx={{
|
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default memo(ImageGalleryGrid);
|
|
@ -0,0 +1,128 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
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 IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback';
|
||||||
|
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
||||||
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
|
import {
|
||||||
|
imageRangeEndSelected,
|
||||||
|
imageSelected,
|
||||||
|
imageSelectionToggled,
|
||||||
|
imagesRemovedFromBatch,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
const makeSelector = (image_name: string) =>
|
||||||
|
createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
(state) => ({
|
||||||
|
selectionCount: state.gallery.selection.length,
|
||||||
|
selection: state.gallery.selection,
|
||||||
|
isSelected: state.gallery.selection.includes(image_name),
|
||||||
|
}),
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
type BatchImageProps = {
|
||||||
|
imageName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BatchImage = (props: BatchImageProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { imageName } = props;
|
||||||
|
const {
|
||||||
|
currentData: imageDTO,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
isSuccess,
|
||||||
|
} = useGetImageDTOQuery(imageName);
|
||||||
|
const selector = useMemo(() => makeSelector(imageName), [imageName]);
|
||||||
|
|
||||||
|
const { isSelected, selectionCount, selection } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleClickRemove = useCallback(() => {
|
||||||
|
dispatch(imagesRemovedFromBatch([imageName]));
|
||||||
|
}, [dispatch, imageName]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
dispatch(imageRangeEndSelected(imageName));
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
dispatch(imageSelectionToggled(imageName));
|
||||||
|
} else {
|
||||||
|
dispatch(imageSelected(imageName));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, imageName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||||
|
if (selectionCount > 1) {
|
||||||
|
return {
|
||||||
|
id: 'batch',
|
||||||
|
payloadType: 'IMAGE_NAMES',
|
||||||
|
payload: { image_names: selection },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageDTO) {
|
||||||
|
return {
|
||||||
|
id: 'batch',
|
||||||
|
payloadType: 'IMAGE_DTO',
|
||||||
|
payload: { imageDTO },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [imageDTO, selection, selectionCount]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <IAIFillSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !imageDTO) {
|
||||||
|
return <IAIErrorLoadingImageFallback />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||||
|
<ImageContextMenu imageDTO={imageDTO}>
|
||||||
|
{(ref) => (
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
key={imageName}
|
||||||
|
userSelect="none"
|
||||||
|
ref={ref}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
aspectRatio: '1/1',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IAIDndImage
|
||||||
|
onClick={handleClick}
|
||||||
|
imageDTO={imageDTO}
|
||||||
|
draggableData={draggableData}
|
||||||
|
isSelected={isSelected}
|
||||||
|
minSize={0}
|
||||||
|
onClickReset={handleClickRemove}
|
||||||
|
isDropDisabled={true}
|
||||||
|
imageSx={{ w: 'full', h: 'full' }}
|
||||||
|
isUploadDisabled={true}
|
||||||
|
resetTooltip="Remove from batch"
|
||||||
|
withResetIcon
|
||||||
|
thumbnail
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ImageContextMenu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(BatchImage);
|
@ -0,0 +1,87 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
|
|
||||||
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaImage } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { VirtuosoGrid } from 'react-virtuoso';
|
||||||
|
import BatchImage from './BatchImage';
|
||||||
|
import ItemContainer from './ImageGridItemContainer';
|
||||||
|
import ListContainer from './ImageGridListContainer';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
(state) => {
|
||||||
|
return {
|
||||||
|
imageNames: state.gallery.batchImageNames,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const BatchImageGrid = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const rootRef = useRef(null);
|
||||||
|
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||||
|
const [initialize, osInstance] = useOverlayScrollbars({
|
||||||
|
defer: true,
|
||||||
|
options: {
|
||||||
|
scrollbars: {
|
||||||
|
visibility: 'auto',
|
||||||
|
autoHide: 'leave',
|
||||||
|
autoHideDelay: 1300,
|
||||||
|
theme: 'os-theme-dark',
|
||||||
|
},
|
||||||
|
overflow: { x: 'hidden' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { imageNames } = useAppSelector(selector);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { current: root } = rootRef;
|
||||||
|
if (scroller && root) {
|
||||||
|
initialize({
|
||||||
|
target: root,
|
||||||
|
elements: {
|
||||||
|
viewport: scroller,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => osInstance()?.destroy();
|
||||||
|
}, [scroller, initialize, osInstance]);
|
||||||
|
|
||||||
|
if (imageNames.length) {
|
||||||
|
return (
|
||||||
|
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||||
|
<VirtuosoGrid
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
data={imageNames}
|
||||||
|
components={{
|
||||||
|
Item: ItemContainer,
|
||||||
|
List: ListContainer,
|
||||||
|
}}
|
||||||
|
scrollerRef={setScroller}
|
||||||
|
itemContent={(index, imageName) => (
|
||||||
|
<BatchImage key={imageName} imageName={imageName} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAINoContentFallback
|
||||||
|
label={t('gallery.noImagesInGallery')}
|
||||||
|
icon={FaImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(BatchImageGrid);
|
@ -1,69 +1,57 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, Spinner } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaTrash } from 'react-icons/fa';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import {
|
import {
|
||||||
imageRangeEndSelected,
|
imageRangeEndSelected,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
imageSelectionToggled,
|
imageSelectionToggled,
|
||||||
} from '../store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import ImageContextMenu from './ImageContextMenu';
|
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
export const makeSelector = (image_name: string) =>
|
export const makeSelector = (image_name: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ gallery }) => {
|
({ gallery }) => ({
|
||||||
const isSelected = gallery.selection.includes(image_name);
|
isSelected: gallery.selection.includes(image_name),
|
||||||
const selectionCount = gallery.selection.length;
|
selectionCount: gallery.selection.length,
|
||||||
|
selection: gallery.selection,
|
||||||
return {
|
}),
|
||||||
isSelected,
|
|
||||||
selectionCount,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
imageDTO: ImageDTO;
|
imageName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gallery image component with delete/use all/use seed buttons on hover.
|
|
||||||
*/
|
|
||||||
const GalleryImage = (props: HoverableImageProps) => {
|
const GalleryImage = (props: HoverableImageProps) => {
|
||||||
const { imageDTO } = props;
|
|
||||||
const { image_url, thumbnail_url, image_name } = imageDTO;
|
|
||||||
|
|
||||||
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
|
|
||||||
|
|
||||||
const { isSelected, selectionCount } = useAppSelector(localSelector);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { imageName } = props;
|
||||||
|
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
||||||
|
const localSelector = useMemo(() => makeSelector(imageName), [imageName]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { isSelected, selectionCount, selection } =
|
||||||
|
useAppSelector(localSelector);
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: MouseEvent<HTMLDivElement>) => {
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
// multiselect disabled for now
|
// disable multiselect for now
|
||||||
// if (e.shiftKey) {
|
// if (e.shiftKey) {
|
||||||
// dispatch(imageRangeEndSelected(props.imageDTO.image_name));
|
// dispatch(imageRangeEndSelected(imageName));
|
||||||
// } else if (e.ctrlKey || e.metaKey) {
|
// } else if (e.ctrlKey || e.metaKey) {
|
||||||
// dispatch(imageSelectionToggled(props.imageDTO.image_name));
|
// dispatch(imageSelectionToggled(imageName));
|
||||||
// } else {
|
// } else {
|
||||||
// dispatch(imageSelected(props.imageDTO.image_name));
|
// dispatch(imageSelected(imageName));
|
||||||
// }
|
// }
|
||||||
dispatch(imageSelected(props.imageDTO.image_name));
|
dispatch(imageSelected(imageName));
|
||||||
},
|
},
|
||||||
[dispatch, props.imageDTO.image_name]
|
[dispatch, imageName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
@ -81,7 +69,8 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
if (selectionCount > 1) {
|
if (selectionCount > 1) {
|
||||||
return {
|
return {
|
||||||
id: 'gallery-image',
|
id: 'gallery-image',
|
||||||
payloadType: 'GALLERY_SELECTION',
|
payloadType: 'IMAGE_NAMES',
|
||||||
|
payload: { image_names: selection },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,15 +81,19 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
payload: { imageDTO },
|
payload: { imageDTO },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [imageDTO, selectionCount]);
|
}, [imageDTO, selection, selectionCount]);
|
||||||
|
|
||||||
|
if (!imageDTO) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||||
<ImageContextMenu image={imageDTO}>
|
<ImageContextMenu imageDTO={imageDTO}>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Box
|
<Box
|
||||||
position="relative"
|
position="relative"
|
||||||
key={image_name}
|
key={imageName}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sx={{
|
sx={{
|
||||||
@ -117,13 +110,13 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
minSize={0}
|
minSize={0}
|
||||||
onClickReset={handleDelete}
|
onClickReset={handleDelete}
|
||||||
resetIcon={<FaTrash />}
|
|
||||||
resetTooltip="Delete image"
|
|
||||||
imageSx={{ w: 'full', h: 'full' }}
|
imageSx={{ w: 'full', h: 'full' }}
|
||||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
|
||||||
isDropDisabled={true}
|
isDropDisabled={true}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
thumbnail={true}
|
thumbnail={true}
|
||||||
|
// resetIcon={<FaTrash />}
|
||||||
|
// resetTooltip="Delete image"
|
||||||
|
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
@ -0,0 +1,204 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
|
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaImage } from 'react-icons/fa';
|
||||||
|
import GalleryImage from './GalleryImage';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
|
IMAGE_LIMIT,
|
||||||
|
selectImagesAll,
|
||||||
|
} from 'features/gallery//store/gallerySlice';
|
||||||
|
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { VirtuosoGrid } from 'react-virtuoso';
|
||||||
|
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||||
|
import ImageGridItemContainer from './ImageGridItemContainer';
|
||||||
|
import ImageGridListContainer from './ImageGridListContainer';
|
||||||
|
import { useListBoardImagesQuery } from '../../../../services/api/endpoints/boardImages';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[stateSelector, selectFilteredImages],
|
||||||
|
(state, filteredImages) => {
|
||||||
|
const {
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
selectedBoardId,
|
||||||
|
galleryView,
|
||||||
|
total,
|
||||||
|
isLoading,
|
||||||
|
} = state.gallery;
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageNames: filteredImages.map((i) => i.image_name),
|
||||||
|
total,
|
||||||
|
selectedBoardId,
|
||||||
|
galleryView,
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const GalleryImageGrid = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const emptyGalleryRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||||
|
const [initialize, osInstance] = useOverlayScrollbars({
|
||||||
|
defer: true,
|
||||||
|
options: {
|
||||||
|
scrollbars: {
|
||||||
|
visibility: 'auto',
|
||||||
|
autoHide: 'leave',
|
||||||
|
autoHideDelay: 1300,
|
||||||
|
theme: 'os-theme-dark',
|
||||||
|
},
|
||||||
|
overflow: { x: 'hidden' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [didInitialFetch, setDidInitialFetch] = useState(false);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
imageNames: imageNamesAll, //all images names loaded on main tab,
|
||||||
|
total: totalAll,
|
||||||
|
selectedBoardId,
|
||||||
|
galleryView,
|
||||||
|
isLoading: isLoadingAll,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
|
const { data: imagesForBoard, isLoading: isLoadingImagesForBoard } =
|
||||||
|
useListBoardImagesQuery(
|
||||||
|
{ board_id: selectedBoardId },
|
||||||
|
{ skip: selectedBoardId === 'all' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageNames = useMemo(() => {
|
||||||
|
if (selectedBoardId === 'all') {
|
||||||
|
return imageNamesAll; // already sorted by images/uploads in gallery selector
|
||||||
|
} else {
|
||||||
|
const categories =
|
||||||
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
|
const imageList = (imagesForBoard?.items || []).filter((img) =>
|
||||||
|
categories.includes(img.image_category)
|
||||||
|
);
|
||||||
|
return imageList.map((img) => img.image_name);
|
||||||
|
}
|
||||||
|
}, [selectedBoardId, galleryView, imagesForBoard, imageNamesAll]);
|
||||||
|
|
||||||
|
const areMoreAvailable = useMemo(() => {
|
||||||
|
return selectedBoardId === 'all' ? totalAll > imageNamesAll.length : false;
|
||||||
|
}, [selectedBoardId, imageNamesAll.length, totalAll]);
|
||||||
|
|
||||||
|
const isLoading = useMemo(() => {
|
||||||
|
return selectedBoardId === 'all' ? isLoadingAll : isLoadingImagesForBoard;
|
||||||
|
}, [selectedBoardId, isLoadingAll, isLoadingImagesForBoard]);
|
||||||
|
|
||||||
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
receivedPageOfImages({
|
||||||
|
categories:
|
||||||
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
|
||||||
|
is_intermediate: false,
|
||||||
|
offset: imageNames.length,
|
||||||
|
limit: IMAGE_LIMIT,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, imageNames.length, galleryView]);
|
||||||
|
|
||||||
|
const handleEndReached = useMemo(() => {
|
||||||
|
if (areMoreAvailable) {
|
||||||
|
return handleLoadMoreImages;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [areMoreAvailable, handleLoadMoreImages]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!didInitialFetch) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // rough, conservative calculation of how many images fit in the gallery
|
||||||
|
// // TODO: this gets an incorrect value on first load...
|
||||||
|
// const galleryHeight = rootRef.current?.clientHeight ?? 0;
|
||||||
|
// const galleryWidth = rootRef.current?.clientHeight ?? 0;
|
||||||
|
|
||||||
|
// const rows = galleryHeight / galleryImageMinimumWidth;
|
||||||
|
// const columns = galleryWidth / galleryImageMinimumWidth;
|
||||||
|
|
||||||
|
// const imagesToLoad = Math.ceil(rows * columns);
|
||||||
|
|
||||||
|
// setDidInitialFetch(true);
|
||||||
|
|
||||||
|
// // load up that many images
|
||||||
|
// dispatch(
|
||||||
|
// receivedPageOfImages({
|
||||||
|
// offset: 0,
|
||||||
|
// limit: 10,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }, [
|
||||||
|
// didInitialFetch,
|
||||||
|
// dispatch,
|
||||||
|
// galleryImageMinimumWidth,
|
||||||
|
// galleryView,
|
||||||
|
// selectedBoardId,
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
if (!isLoading && imageNames.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}>
|
||||||
|
<IAINoContentFallback
|
||||||
|
label={t('gallery.noImagesInGallery')}
|
||||||
|
icon={FaImage}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log({ selectedBoardId });
|
||||||
|
|
||||||
|
if (status !== 'rejected') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||||
|
<VirtuosoGrid
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
data={imageNames}
|
||||||
|
components={{
|
||||||
|
Item: ImageGridItemContainer,
|
||||||
|
List: ImageGridListContainer,
|
||||||
|
}}
|
||||||
|
scrollerRef={setScroller}
|
||||||
|
itemContent={(index, imageName) => (
|
||||||
|
<GalleryImage key={imageName} imageName={imageName} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<IAIButton
|
||||||
|
onClick={handleLoadMoreImages}
|
||||||
|
isDisabled={!areMoreAvailable}
|
||||||
|
isLoading={status === 'pending'}
|
||||||
|
loadingText="Loading"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
{areMoreAvailable
|
||||||
|
? t('gallery.loadMore')
|
||||||
|
: t('gallery.allImagesLoaded')}
|
||||||
|
</IAIButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(GalleryImageGrid);
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Box, FlexProps, forwardRef } from '@chakra-ui/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||||
|
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||||
|
<Box className="item-container" ref={ref} p={1.5}>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ItemContainer;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { FlexProps, Grid, forwardRef } from '@chakra-ui/react';
|
||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||||
|
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||||
|
const galleryImageMinimumWidth = useAppSelector(
|
||||||
|
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
{...props}
|
||||||
|
className="list-container"
|
||||||
|
ref={ref}
|
||||||
|
sx={{
|
||||||
|
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ListContainer;
|
@ -1,7 +1,7 @@
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { UnsafeImageMetadata } from 'services/api/endpoints/images';
|
import { UnsafeImageMetadata } from 'services/api/endpoints/images';
|
||||||
import MetadataItem from './MetadataItem';
|
import ImageMetadataItem from './ImageMetadataItem';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
metadata?: UnsafeImageMetadata['metadata'];
|
metadata?: UnsafeImageMetadata['metadata'];
|
||||||
@ -73,13 +73,13 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{metadata.generation_mode && (
|
{metadata.generation_mode && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Generation Mode"
|
label="Generation Mode"
|
||||||
value={metadata.generation_mode}
|
value={metadata.generation_mode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.positive_prompt && (
|
{metadata.positive_prompt && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Positive Prompt"
|
label="Positive Prompt"
|
||||||
labelPosition="top"
|
labelPosition="top"
|
||||||
value={metadata.positive_prompt}
|
value={metadata.positive_prompt}
|
||||||
@ -87,7 +87,7 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.negative_prompt && (
|
{metadata.negative_prompt && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Negative Prompt"
|
label="Negative Prompt"
|
||||||
labelPosition="top"
|
labelPosition="top"
|
||||||
value={metadata.negative_prompt}
|
value={metadata.negative_prompt}
|
||||||
@ -95,28 +95,28 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.seed !== undefined && (
|
{metadata.seed !== undefined && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Seed"
|
label="Seed"
|
||||||
value={metadata.seed}
|
value={metadata.seed}
|
||||||
onClick={handleRecallSeed}
|
onClick={handleRecallSeed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.model !== undefined && (
|
{metadata.model !== undefined && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Model"
|
label="Model"
|
||||||
value={metadata.model.model_name}
|
value={metadata.model.model_name}
|
||||||
onClick={handleRecallModel}
|
onClick={handleRecallModel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.width && (
|
{metadata.width && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Width"
|
label="Width"
|
||||||
value={metadata.width}
|
value={metadata.width}
|
||||||
onClick={handleRecallWidth}
|
onClick={handleRecallWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.height && (
|
{metadata.height && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Height"
|
label="Height"
|
||||||
value={metadata.height}
|
value={metadata.height}
|
||||||
onClick={handleRecallHeight}
|
onClick={handleRecallHeight}
|
||||||
@ -137,21 +137,21 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)} */}
|
)} */}
|
||||||
{metadata.scheduler && (
|
{metadata.scheduler && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Scheduler"
|
label="Scheduler"
|
||||||
value={metadata.scheduler}
|
value={metadata.scheduler}
|
||||||
onClick={handleRecallScheduler}
|
onClick={handleRecallScheduler}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.steps && (
|
{metadata.steps && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Steps"
|
label="Steps"
|
||||||
value={metadata.steps}
|
value={metadata.steps}
|
||||||
onClick={handleRecallSteps}
|
onClick={handleRecallSteps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{metadata.cfg_scale !== undefined && (
|
{metadata.cfg_scale !== undefined && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="CFG scale"
|
label="CFG scale"
|
||||||
value={metadata.cfg_scale}
|
value={metadata.cfg_scale}
|
||||||
onClick={handleRecallCfgScale}
|
onClick={handleRecallCfgScale}
|
||||||
@ -192,7 +192,7 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)} */}
|
)} */}
|
||||||
{metadata.strength && (
|
{metadata.strength && (
|
||||||
<MetadataItem
|
<ImageMetadataItem
|
||||||
label="Image to image strength"
|
label="Image to image strength"
|
||||||
value={metadata.strength}
|
value={metadata.strength}
|
||||||
onClick={handleRecallStrength}
|
onClick={handleRecallStrength}
|
@ -16,7 +16,7 @@ type MetadataItemProps = {
|
|||||||
/**
|
/**
|
||||||
* Component to display an individual metadata item or parameter.
|
* Component to display an individual metadata item or parameter.
|
||||||
*/
|
*/
|
||||||
const MetadataItem = ({
|
const ImageMetadataItem = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onClick,
|
onClick,
|
||||||
@ -74,4 +74,4 @@ const MetadataItem = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MetadataItem;
|
export default ImageMetadataItem;
|
@ -8,7 +8,7 @@ type Props = {
|
|||||||
jsonObject: object;
|
jsonObject: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetadataJSONViewer = (props: Props) => {
|
const ImageMetadataJSON = (props: Props) => {
|
||||||
const { copyTooltip, jsonObject } = props;
|
const { copyTooltip, jsonObject } = props;
|
||||||
const jsonString = useMemo(
|
const jsonString = useMemo(
|
||||||
() => JSON.stringify(jsonObject, null, 2),
|
() => JSON.stringify(jsonObject, null, 2),
|
||||||
@ -67,4 +67,4 @@ const MetadataJSONViewer = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MetadataJSONViewer;
|
export default ImageMetadataJSON;
|
@ -15,7 +15,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
|||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import ImageMetadataActions from './ImageMetadataActions';
|
import ImageMetadataActions from './ImageMetadataActions';
|
||||||
import MetadataJSONViewer from './MetadataJSONViewer';
|
import ImageMetadataJSON from './ImageMetadataJSON';
|
||||||
|
|
||||||
type ImageMetadataViewerProps = {
|
type ImageMetadataViewerProps = {
|
||||||
image: ImageDTO;
|
image: ImageDTO;
|
||||||
@ -123,7 +123,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
key={tab.label}
|
key={tab.label}
|
||||||
sx={{ w: 'full', h: 'full', p: 0, pt: 4 }}
|
sx={{ w: 'full', h: 'full', p: 0, pt: 4 }}
|
||||||
>
|
>
|
||||||
<MetadataJSONViewer
|
<ImageMetadataJSON
|
||||||
jsonObject={tab.data}
|
jsonObject={tab.data}
|
||||||
copyTooltip={tab.copyTooltip}
|
copyTooltip={tab.copyTooltip}
|
||||||
/>
|
/>
|
@ -1,17 +1,12 @@
|
|||||||
import { ChakraProps, Flex, Grid, IconButton, Spinner } from '@chakra-ui/react';
|
import { Box, ChakraProps, Flex, IconButton, Spinner } from '@chakra-ui/react';
|
||||||
import { memo, useCallback, useState } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||||
import { useNextPrevImage } from '../hooks/useNextPrevImage';
|
import { useNextPrevImage } from '../hooks/useNextPrevImage';
|
||||||
|
|
||||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
|
||||||
height: '100%',
|
|
||||||
width: '15%',
|
|
||||||
alignItems: 'center',
|
|
||||||
pointerEvents: 'auto',
|
|
||||||
};
|
|
||||||
const nextPrevButtonStyles: ChakraProps['sx'] = {
|
const nextPrevButtonStyles: ChakraProps['sx'] = {
|
||||||
color: 'base.100',
|
color: 'base.100',
|
||||||
|
pointerEvents: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
const NextPrevImageButtons = () => {
|
const NextPrevImageButtons = () => {
|
||||||
@ -27,35 +22,23 @@ const NextPrevImageButtons = () => {
|
|||||||
isFetching,
|
isFetching,
|
||||||
} = useNextPrevImage();
|
} = useNextPrevImage();
|
||||||
|
|
||||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleCurrentImagePreviewMouseOver = useCallback(() => {
|
|
||||||
setShouldShowNextPrevButtons(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCurrentImagePreviewMouseOut = useCallback(() => {
|
|
||||||
setShouldShowNextPrevButtons(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'space-between',
|
position: 'relative',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
...nextPrevButtonTriggerAreaStyles,
|
pos: 'absolute',
|
||||||
justifyContent: 'flex-start',
|
top: '50%',
|
||||||
|
transform: 'translate(0, -50%)',
|
||||||
|
insetInlineStart: 0,
|
||||||
}}
|
}}
|
||||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
|
||||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
|
||||||
>
|
>
|
||||||
{shouldShowNextPrevButtons && !isOnFirstImage && (
|
{!isOnFirstImage && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('accessibility.previousImage')}
|
aria-label={t('accessibility.previousImage')}
|
||||||
icon={<FaAngleLeft size={64} />}
|
icon={<FaAngleLeft size={64} />}
|
||||||
@ -65,16 +48,16 @@ const NextPrevImageButtons = () => {
|
|||||||
sx={nextPrevButtonStyles}
|
sx={nextPrevButtonStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Box>
|
||||||
<Grid
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
...nextPrevButtonTriggerAreaStyles,
|
pos: 'absolute',
|
||||||
justifyContent: 'flex-end',
|
top: '50%',
|
||||||
|
transform: 'translate(0, -50%)',
|
||||||
|
insetInlineEnd: 0,
|
||||||
}}
|
}}
|
||||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
|
||||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
|
||||||
>
|
>
|
||||||
{shouldShowNextPrevButtons && !isOnLastImage && (
|
{!isOnLastImage && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('accessibility.nextImage')}
|
aria-label={t('accessibility.nextImage')}
|
||||||
icon={<FaAngleRight size={64} />}
|
icon={<FaAngleRight size={64} />}
|
||||||
@ -84,36 +67,30 @@ const NextPrevImageButtons = () => {
|
|||||||
sx={nextPrevButtonStyles}
|
sx={nextPrevButtonStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shouldShowNextPrevButtons &&
|
{isOnLastImage && areMoreImagesAvailable && !isFetching && (
|
||||||
isOnLastImage &&
|
<IconButton
|
||||||
areMoreImagesAvailable &&
|
aria-label={t('accessibility.loadMore')}
|
||||||
!isFetching && (
|
icon={<FaAngleDoubleRight size={64} />}
|
||||||
<IconButton
|
variant="unstyled"
|
||||||
aria-label={t('accessibility.loadMore')}
|
onClick={handleLoadMoreImages}
|
||||||
icon={<FaAngleDoubleRight size={64} />}
|
boxSize={16}
|
||||||
variant="unstyled"
|
sx={nextPrevButtonStyles}
|
||||||
onClick={handleLoadMoreImages}
|
/>
|
||||||
boxSize={16}
|
)}
|
||||||
sx={nextPrevButtonStyles}
|
{isOnLastImage && areMoreImagesAvailable && isFetching && (
|
||||||
/>
|
<Flex
|
||||||
)}
|
sx={{
|
||||||
{shouldShowNextPrevButtons &&
|
w: 16,
|
||||||
isOnLastImage &&
|
h: 16,
|
||||||
areMoreImagesAvailable &&
|
alignItems: 'center',
|
||||||
isFetching && (
|
justifyContent: 'center',
|
||||||
<Flex
|
}}
|
||||||
sx={{
|
>
|
||||||
w: 16,
|
<Spinner opacity={0.5} size="xl" />
|
||||||
h: 16,
|
</Flex>
|
||||||
alignItems: 'center',
|
)}
|
||||||
justifyContent: 'center',
|
</Box>
|
||||||
}}
|
</Box>
|
||||||
>
|
|
||||||
<Spinner opacity={0.5} size="xl" />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectFilteredImages,
|
|
||||||
selectImagesById,
|
selectImagesById,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||||
|
import { selectFilteredImages } from '../store/gallerySelectors';
|
||||||
|
|
||||||
export const nextPrevImageButtonsSelector = createSelector(
|
export const nextPrevImageButtonsSelector = createSelector(
|
||||||
[stateSelector, selectFilteredImages],
|
[stateSelector, selectFilteredImages],
|
||||||
|
@ -11,7 +11,6 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [
|
|||||||
'limit',
|
'limit',
|
||||||
'offset',
|
'offset',
|
||||||
'selectedBoardId',
|
'selectedBoardId',
|
||||||
'categories',
|
|
||||||
'galleryView',
|
'galleryView',
|
||||||
'total',
|
'total',
|
||||||
'isInitialized',
|
'isInitialized',
|
||||||
|
@ -1,3 +1,136 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { clamp, keyBy } from 'lodash-es';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
BoardId,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
|
imagesAdapter,
|
||||||
|
initialGalleryState,
|
||||||
|
} from './gallerySlice';
|
||||||
|
|
||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
export const gallerySelector = (state: RootState) => state.gallery;
|
||||||
|
|
||||||
|
const isInSelectedBoard = (
|
||||||
|
selectedBoardId: BoardId,
|
||||||
|
imageDTO: ImageDTO,
|
||||||
|
batchImageNames: string[]
|
||||||
|
) => {
|
||||||
|
if (selectedBoardId === 'all') {
|
||||||
|
// all images are in the "All Images" board
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedBoardId === 'none' && !imageDTO.board_id) {
|
||||||
|
// Only images without a board are in the "No Board" board
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedBoardId === 'batch' &&
|
||||||
|
batchImageNames.includes(imageDTO.image_name)
|
||||||
|
) {
|
||||||
|
// Only images with is_batch are in the "Batch" board
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedBoardId === imageDTO.board_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectFilteredImagesLocal = createSelector(
|
||||||
|
[(state: typeof initialGalleryState) => state],
|
||||||
|
(galleryState) => {
|
||||||
|
const allImages = imagesAdapter.getSelectors().selectAll(galleryState);
|
||||||
|
const { galleryView, selectedBoardId } = galleryState;
|
||||||
|
|
||||||
|
const categories =
|
||||||
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
|
|
||||||
|
const filteredImages = allImages.filter((i) => {
|
||||||
|
const isInCategory = categories.includes(i.image_category);
|
||||||
|
|
||||||
|
const isInBoard = isInSelectedBoard(
|
||||||
|
selectedBoardId,
|
||||||
|
i,
|
||||||
|
galleryState.batchImageNames
|
||||||
|
);
|
||||||
|
return isInCategory && isInBoard;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredImages;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectFilteredImages = createSelector(
|
||||||
|
(state: RootState) => state,
|
||||||
|
(state) => {
|
||||||
|
return selectFilteredImagesLocal(state.gallery);
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectFilteredImagesAsObject = createSelector(
|
||||||
|
selectFilteredImages,
|
||||||
|
(filteredImages) => keyBy(filteredImages, 'image_name')
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectFilteredImagesIds = createSelector(
|
||||||
|
selectFilteredImages,
|
||||||
|
(filteredImages) => filteredImages.map((i) => i.image_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectLastSelectedImage = createSelector(
|
||||||
|
(state: RootState) => state,
|
||||||
|
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectSelectedImages = createSelector(
|
||||||
|
(state: RootState) => state,
|
||||||
|
(state) =>
|
||||||
|
imagesAdapter
|
||||||
|
.getSelectors()
|
||||||
|
.selectAll(state.gallery)
|
||||||
|
.filter((i) => state.gallery.selection.includes(i.image_name)),
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectNextImageToSelectLocal = createSelector(
|
||||||
|
[
|
||||||
|
(state: typeof initialGalleryState) => state,
|
||||||
|
(state: typeof initialGalleryState, image_name: string) => image_name,
|
||||||
|
],
|
||||||
|
(state, image_name) => {
|
||||||
|
const filteredImages = selectFilteredImagesLocal(state);
|
||||||
|
const ids = filteredImages.map((i) => i.image_name);
|
||||||
|
|
||||||
|
const deletedImageIndex = ids.findIndex(
|
||||||
|
(result) => result.toString() === image_name
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredIds = ids.filter((id) => id.toString() !== image_name);
|
||||||
|
|
||||||
|
const newSelectedImageIndex = clamp(
|
||||||
|
deletedImageIndex,
|
||||||
|
0,
|
||||||
|
filteredIds.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||||
|
|
||||||
|
return newSelectedImageId;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectNextImageToSelect = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState) => state,
|
||||||
|
(state: RootState, image_name: string) => image_name,
|
||||||
|
],
|
||||||
|
(state, image_name) => {
|
||||||
|
return selectNextImageToSelectLocal(state.gallery, image_name);
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import type { PayloadAction, Update } from '@reduxjs/toolkit';
|
import type { PayloadAction, Update } from '@reduxjs/toolkit';
|
||||||
import {
|
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||||
createEntityAdapter,
|
|
||||||
createSelector,
|
|
||||||
createSlice,
|
|
||||||
} from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { dateComparator } from 'common/util/dateComparator';
|
import { dateComparator } from 'common/util/dateComparator';
|
||||||
import { keyBy, uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import {
|
import {
|
||||||
imageUrlsReceived,
|
imageUrlsReceived,
|
||||||
receivedPageOfImages,
|
receivedPageOfImages,
|
||||||
} from 'services/api/thunks/image';
|
} from 'services/api/thunks/image';
|
||||||
import { ImageCategory, ImageDTO } from 'services/api/types';
|
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
|
import { selectFilteredImagesLocal } from './gallerySelectors';
|
||||||
|
|
||||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||||
selectId: (image) => image.image_name,
|
selectId: (image) => image.image_name,
|
||||||
@ -27,23 +23,30 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
|||||||
'user',
|
'user',
|
||||||
'other',
|
'other',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const INITIAL_IMAGE_LIMIT = 100;
|
export const INITIAL_IMAGE_LIMIT = 100;
|
||||||
export const IMAGE_LIMIT = 20;
|
export const IMAGE_LIMIT = 20;
|
||||||
|
|
||||||
|
export type GalleryView = 'images' | 'assets';
|
||||||
|
export type BoardId =
|
||||||
|
| 'all'
|
||||||
|
| 'none'
|
||||||
|
| 'batch'
|
||||||
|
| (string & Record<never, never>);
|
||||||
|
|
||||||
type AdditionaGalleryState = {
|
type AdditionaGalleryState = {
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
categories: ImageCategory[];
|
|
||||||
selectedBoardId?: string;
|
|
||||||
selection: string[];
|
selection: string[];
|
||||||
shouldAutoSwitch: boolean;
|
shouldAutoSwitch: boolean;
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
galleryView: 'images' | 'assets';
|
galleryView: GalleryView;
|
||||||
|
selectedBoardId: BoardId;
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
|
batchImageNames: string[];
|
||||||
|
isBatchEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialGalleryState =
|
export const initialGalleryState =
|
||||||
@ -53,12 +56,14 @@ export const initialGalleryState =
|
|||||||
total: 0,
|
total: 0,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isFetching: true,
|
isFetching: true,
|
||||||
categories: IMAGE_CATEGORIES,
|
|
||||||
selection: [],
|
selection: [],
|
||||||
shouldAutoSwitch: true,
|
shouldAutoSwitch: true,
|
||||||
galleryImageMinimumWidth: 96,
|
galleryImageMinimumWidth: 96,
|
||||||
galleryView: 'images',
|
galleryView: 'images',
|
||||||
|
selectedBoardId: 'all',
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
|
batchImageNames: [],
|
||||||
|
isBatchEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -73,7 +78,7 @@ export const gallerySlice = createSlice({
|
|||||||
) {
|
) {
|
||||||
state.selection = [action.payload.image_name];
|
state.selection = [action.payload.image_name];
|
||||||
state.galleryView = 'images';
|
state.galleryView = 'images';
|
||||||
state.categories = IMAGE_CATEGORIES;
|
state.selectedBoardId = 'all';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
||||||
@ -81,12 +86,15 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
imageRemoved: (state, action: PayloadAction<string>) => {
|
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||||
imagesAdapter.removeOne(state, action.payload);
|
imagesAdapter.removeOne(state, action.payload);
|
||||||
|
state.batchImageNames = state.batchImageNames.filter(
|
||||||
|
(name) => name !== action.payload
|
||||||
|
);
|
||||||
},
|
},
|
||||||
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
||||||
imagesAdapter.removeMany(state, action.payload);
|
imagesAdapter.removeMany(state, action.payload);
|
||||||
},
|
state.batchImageNames = state.batchImageNames.filter(
|
||||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
(name) => !action.payload.includes(name)
|
||||||
state.categories = action.payload;
|
);
|
||||||
},
|
},
|
||||||
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
||||||
const rangeEndImageName = action.payload;
|
const rangeEndImageName = action.payload;
|
||||||
@ -127,9 +135,7 @@ export const gallerySlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
imageSelected: (state, action: PayloadAction<string | null>) => {
|
imageSelected: (state, action: PayloadAction<string | null>) => {
|
||||||
state.selection = action.payload
|
state.selection = action.payload ? [action.payload] : [];
|
||||||
? [action.payload]
|
|
||||||
: [String(state.ids[0])];
|
|
||||||
},
|
},
|
||||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitch = action.payload;
|
state.shouldAutoSwitch = action.payload;
|
||||||
@ -137,15 +143,43 @@ export const gallerySlice = createSlice({
|
|||||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||||
state.galleryImageMinimumWidth = action.payload;
|
state.galleryImageMinimumWidth = action.payload;
|
||||||
},
|
},
|
||||||
setGalleryView: (state, action: PayloadAction<'images' | 'assets'>) => {
|
setGalleryView: (state, action: PayloadAction<GalleryView>) => {
|
||||||
state.galleryView = action.payload;
|
state.galleryView = action.payload;
|
||||||
},
|
},
|
||||||
boardIdSelected: (state, action: PayloadAction<string | undefined>) => {
|
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||||
state.selectedBoardId = action.payload;
|
state.selectedBoardId = action.payload;
|
||||||
},
|
},
|
||||||
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isLoading = action.payload;
|
state.isLoading = action.payload;
|
||||||
},
|
},
|
||||||
|
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isBatchEnabled = action.payload;
|
||||||
|
},
|
||||||
|
imagesAddedToBatch: (state, action: PayloadAction<string[]>) => {
|
||||||
|
state.batchImageNames = uniq(
|
||||||
|
state.batchImageNames.concat(action.payload)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imagesRemovedFromBatch: (state, action: PayloadAction<string[]>) => {
|
||||||
|
state.batchImageNames = state.batchImageNames.filter(
|
||||||
|
(imageName) => !action.payload.includes(imageName)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSelection = state.selection.filter(
|
||||||
|
(imageName) => !action.payload.includes(imageName)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newSelection.length) {
|
||||||
|
state.selection = newSelection;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selection = [state.batchImageNames[0]] ?? [];
|
||||||
|
},
|
||||||
|
batchReset: (state) => {
|
||||||
|
state.batchImageNames = [];
|
||||||
|
state.selection = [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(receivedPageOfImages.pending, (state) => {
|
builder.addCase(receivedPageOfImages.pending, (state) => {
|
||||||
@ -188,7 +222,7 @@ export const gallerySlice = createSlice({
|
|||||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||||
(state, action) => {
|
(state, action) => {
|
||||||
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
||||||
state.selectedBoardId = undefined;
|
state.selectedBoardId = 'all';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -208,7 +242,6 @@ export const {
|
|||||||
imageUpdatedOne,
|
imageUpdatedOne,
|
||||||
imageRemoved,
|
imageRemoved,
|
||||||
imagesRemoved,
|
imagesRemoved,
|
||||||
imageCategoriesChanged,
|
|
||||||
imageRangeEndSelected,
|
imageRangeEndSelected,
|
||||||
imageSelectionToggled,
|
imageSelectionToggled,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
@ -217,48 +250,9 @@ export const {
|
|||||||
setGalleryView,
|
setGalleryView,
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
isLoadingChanged,
|
isLoadingChanged,
|
||||||
|
isBatchEnabledChanged,
|
||||||
|
imagesAddedToBatch,
|
||||||
|
imagesRemovedFromBatch,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
|
||||||
export const selectFilteredImagesLocal = createSelector(
|
|
||||||
(state: typeof initialGalleryState) => state,
|
|
||||||
(galleryState) => {
|
|
||||||
const allImages = imagesAdapter.getSelectors().selectAll(galleryState);
|
|
||||||
const { categories, selectedBoardId } = galleryState;
|
|
||||||
|
|
||||||
const filteredImages = allImages.filter((i) => {
|
|
||||||
const isInCategory = categories.includes(i.image_category);
|
|
||||||
const isInSelectedBoard = selectedBoardId
|
|
||||||
? i.board_id === selectedBoardId
|
|
||||||
: true;
|
|
||||||
return isInCategory && isInSelectedBoard;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredImages;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectFilteredImages = createSelector(
|
|
||||||
(state: RootState) => state,
|
|
||||||
(state) => {
|
|
||||||
return selectFilteredImagesLocal(state.gallery);
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectFilteredImagesAsObject = createSelector(
|
|
||||||
selectFilteredImages,
|
|
||||||
(filteredImages) => keyBy(filteredImages, 'image_name')
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectFilteredImagesIds = createSelector(
|
|
||||||
selectFilteredImages,
|
|
||||||
(filteredImages) => filteredImages.map((i) => i.image_name)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectLastSelectedImage = createSelector(
|
|
||||||
(state: RootState) => state,
|
|
||||||
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons';
|
|
||||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer';
|
|
||||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
|
||||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
|
||||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
|
||||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { BiExit } from 'react-icons/bi';
|
|
||||||
import { TransformWrapper } from 'react-zoom-pan-pinch';
|
|
||||||
import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants';
|
|
||||||
import useImageTransform from '../hooks/useImageTransform';
|
|
||||||
import ReactPanZoomButtons from './ReactPanZoomButtons';
|
|
||||||
import ReactPanZoomImage from './ReactPanZoomImage';
|
|
||||||
|
|
||||||
export const lightboxSelector = createSelector(
|
|
||||||
[gallerySelector, uiSelector],
|
|
||||||
(gallery, ui) => {
|
|
||||||
const { currentImage } = gallery;
|
|
||||||
const { shouldShowImageDetails } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
viewerImageToDisplay: currentImage,
|
|
||||||
shouldShowImageDetails,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Lightbox() {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const isLightBoxOpen = useAppSelector(
|
|
||||||
(state: RootState) => state.lightbox.isLightboxOpen
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
rotation,
|
|
||||||
scaleX,
|
|
||||||
scaleY,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically,
|
|
||||||
rotateCounterClockwise,
|
|
||||||
rotateClockwise,
|
|
||||||
reset,
|
|
||||||
} = useImageTransform();
|
|
||||||
|
|
||||||
const { viewerImageToDisplay, shouldShowImageDetails } =
|
|
||||||
useAppSelector(lightboxSelector);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'Esc',
|
|
||||||
() => {
|
|
||||||
if (isLightBoxOpen) dispatch(setIsLightboxOpen(false));
|
|
||||||
},
|
|
||||||
[isLightBoxOpen]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isLightBoxOpen && (
|
|
||||||
<motion.div
|
|
||||||
key="lightbox"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
width: '100vw',
|
|
||||||
height: `calc(100vh - ${PROGRESS_BAR_THICKNESS * 4}px)`,
|
|
||||||
position: 'fixed',
|
|
||||||
top: `${PROGRESS_BAR_THICKNESS * 4}px`,
|
|
||||||
background: 'var(--invokeai-colors-base-900)',
|
|
||||||
zIndex: 99,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TransformWrapper
|
|
||||||
centerOnInit
|
|
||||||
minScale={0.1}
|
|
||||||
initialPositionX={50}
|
|
||||||
initialPositionY={50}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
flexDir: 'column',
|
|
||||||
position: 'absolute',
|
|
||||||
insetInlineStart: 4,
|
|
||||||
gap: 4,
|
|
||||||
zIndex: 3,
|
|
||||||
top: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiExit />}
|
|
||||||
aria-label="Exit Viewer"
|
|
||||||
className="lightbox-close-btn"
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(setIsLightboxOpen(false));
|
|
||||||
}}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
<ReactPanZoomButtons
|
|
||||||
flipHorizontally={flipHorizontally}
|
|
||||||
flipVertically={flipVertically}
|
|
||||||
rotateCounterClockwise={rotateCounterClockwise}
|
|
||||||
rotateClockwise={rotateClockwise}
|
|
||||||
reset={reset}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 4,
|
|
||||||
zIndex: 3,
|
|
||||||
insetInlineStart: '50%',
|
|
||||||
transform: 'translate(-50%, 0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CurrentImageButtons />
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{viewerImageToDisplay && (
|
|
||||||
<>
|
|
||||||
<ReactPanZoomImage
|
|
||||||
rotation={rotation}
|
|
||||||
scaleX={scaleX}
|
|
||||||
scaleY={scaleY}
|
|
||||||
image={viewerImageToDisplay}
|
|
||||||
styleClass="lightbox-image"
|
|
||||||
/>
|
|
||||||
{shouldShowImageDetails && (
|
|
||||||
<ImageMetadataViewer image={viewerImageToDisplay} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!shouldShowImageDetails && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
insetInlineStart: 0,
|
|
||||||
w: '100vw',
|
|
||||||
h: '100vh',
|
|
||||||
px: 16,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NextPrevImageButtons />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TransformWrapper>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
import { ButtonGroup } from '@chakra-ui/react';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
BiReset,
|
|
||||||
BiRotateLeft,
|
|
||||||
BiRotateRight,
|
|
||||||
BiZoomIn,
|
|
||||||
BiZoomOut,
|
|
||||||
} from 'react-icons/bi';
|
|
||||||
import { MdFlip } from 'react-icons/md';
|
|
||||||
import { useTransformContext } from 'react-zoom-pan-pinch';
|
|
||||||
|
|
||||||
type ReactPanZoomButtonsProps = {
|
|
||||||
flipHorizontally: () => void;
|
|
||||||
flipVertically: () => void;
|
|
||||||
rotateCounterClockwise: () => void;
|
|
||||||
rotateClockwise: () => void;
|
|
||||||
reset: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReactPanZoomButtons = ({
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically,
|
|
||||||
rotateCounterClockwise,
|
|
||||||
rotateClockwise,
|
|
||||||
reset,
|
|
||||||
}: ReactPanZoomButtonsProps) => {
|
|
||||||
const { zoomIn, zoomOut, resetTransform } = useTransformContext();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonGroup isAttached orientation="vertical">
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiZoomIn />}
|
|
||||||
aria-label={t('accessibility.zoomIn')}
|
|
||||||
tooltip={t('accessibility.zoomIn')}
|
|
||||||
onClick={() => zoomIn()}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiZoomOut />}
|
|
||||||
aria-label={t('accessibility.zoomOut')}
|
|
||||||
tooltip={t('accessibility.zoomOut')}
|
|
||||||
onClick={() => zoomOut()}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiRotateLeft />}
|
|
||||||
aria-label={t('accessibility.rotateCounterClockwise')}
|
|
||||||
tooltip={t('accessibility.rotateCounterClockwise')}
|
|
||||||
onClick={rotateCounterClockwise}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiRotateRight />}
|
|
||||||
aria-label={t('accessibility.rotateClockwise')}
|
|
||||||
tooltip={t('accessibility.rotateClockwise')}
|
|
||||||
onClick={rotateClockwise}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<MdFlip />}
|
|
||||||
aria-label={t('accessibility.flipHorizontally')}
|
|
||||||
tooltip={t('accessibility.flipHorizontally')}
|
|
||||||
onClick={flipHorizontally}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<MdFlip style={{ transform: 'rotate(90deg)' }} />}
|
|
||||||
aria-label={t('accessibility.flipVertically')}
|
|
||||||
tooltip={t('accessibility.flipVertically')}
|
|
||||||
onClick={flipVertically}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
icon={<BiReset />}
|
|
||||||
aria-label={t('accessibility.reset')}
|
|
||||||
tooltip={t('accessibility.reset')}
|
|
||||||
onClick={() => {
|
|
||||||
resetTransform();
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
fontSize={20}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReactPanZoomButtons;
|
|
@ -1,46 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
|
|
||||||
type ReactPanZoomProps = {
|
|
||||||
image: ImageDTO;
|
|
||||||
styleClass?: string;
|
|
||||||
alt?: string;
|
|
||||||
ref?: React.Ref<HTMLImageElement>;
|
|
||||||
rotation: number;
|
|
||||||
scaleX: number;
|
|
||||||
scaleY: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ReactPanZoomImage({
|
|
||||||
image,
|
|
||||||
alt,
|
|
||||||
ref,
|
|
||||||
styleClass,
|
|
||||||
rotation,
|
|
||||||
scaleX,
|
|
||||||
scaleY,
|
|
||||||
}: ReactPanZoomProps) {
|
|
||||||
const { centerView } = useTransformContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TransformComponent
|
|
||||||
wrapperStyle={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
src={image.image_url}
|
|
||||||
alt={alt}
|
|
||||||
ref={ref}
|
|
||||||
className={styleClass ? styleClass : ''}
|
|
||||||
onLoad={() => centerView(1, 0, 'easeOut')}
|
|
||||||
/>
|
|
||||||
</TransformComponent>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const useImageTransform = () => {
|
|
||||||
const [rotation, setRotation] = useState(0);
|
|
||||||
const [scaleX, setScaleX] = useState(1);
|
|
||||||
const [scaleY, setScaleY] = useState(1);
|
|
||||||
|
|
||||||
const rotateCounterClockwise = () => {
|
|
||||||
if (rotation === -270) {
|
|
||||||
setRotation(0);
|
|
||||||
} else {
|
|
||||||
setRotation(rotation - 90);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rotateClockwise = () => {
|
|
||||||
if (rotation === 270) {
|
|
||||||
setRotation(0);
|
|
||||||
} else {
|
|
||||||
setRotation(rotation + 90);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const flipHorizontally = () => {
|
|
||||||
setScaleX(scaleX * -1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const flipVertically = () => {
|
|
||||||
setScaleY(scaleY * -1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
setRotation(0);
|
|
||||||
setScaleX(1);
|
|
||||||
setScaleY(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
rotation,
|
|
||||||
scaleX,
|
|
||||||
scaleY,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically,
|
|
||||||
rotateCounterClockwise,
|
|
||||||
rotateClockwise,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useImageTransform;
|
|
@ -1,8 +0,0 @@
|
|||||||
import { LightboxState } from './lightboxSlice';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightbox slice persist denylist
|
|
||||||
*/
|
|
||||||
export const lightboxPersistDenylist: (keyof LightboxState)[] = [
|
|
||||||
'isLightboxOpen',
|
|
||||||
];
|
|
@ -1,13 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
|
|
||||||
export const lightboxSelector = createSelector(
|
|
||||||
(state: RootState) => state.lightbox,
|
|
||||||
(lightbox) => lightbox,
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
equalityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,26 +0,0 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
export interface LightboxState {
|
|
||||||
isLightboxOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialLightboxState: LightboxState = {
|
|
||||||
isLightboxOpen: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState: LightboxState = initialLightboxState;
|
|
||||||
|
|
||||||
export const lightboxSlice = createSlice({
|
|
||||||
name: 'lightbox',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setIsLightboxOpen: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.isLightboxOpen = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setIsLightboxOpen } = lightboxSlice.actions;
|
|
||||||
|
|
||||||
export default lightboxSlice.reducer;
|
|
@ -1,30 +1,24 @@
|
|||||||
import { Flex, Icon, Text } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { FaImage } from 'react-icons/fa';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import {
|
import {
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { initialImage } = state.generation;
|
const { initialImage } = state.generation;
|
||||||
const { asInitialImage: useBatchAsInitialImage, imageNames } = state.batch;
|
|
||||||
return {
|
return {
|
||||||
initialImage,
|
initialImage,
|
||||||
useBatchAsInitialImage,
|
isResetButtonDisabled: !initialImage,
|
||||||
isResetButtonDisabled: useBatchAsInitialImage
|
|
||||||
? imageNames.length === 0
|
|
||||||
: !initialImage,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa';
|
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
|
||||||
import IAIButton from 'common/components/IAIButton';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import {
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
asInitialImageToggled,
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
batchReset,
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
} from 'features/batch/store/batchSlice';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import BatchImageContainer from 'features/batch/components/BatchImageContainer';
|
import useImageUploader from 'common/hooks/useImageUploader';
|
||||||
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
import { PostUploadAction } from 'services/api/thunks/image';
|
||||||
import InitialImage from './InitialImage';
|
import InitialImage from './InitialImage';
|
||||||
|
|
||||||
@ -24,59 +16,34 @@ const selector = createSelector(
|
|||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { initialImage } = state.generation;
|
const { initialImage } = state.generation;
|
||||||
const { asInitialImage: useBatchAsInitialImage, imageNames } = state.batch;
|
|
||||||
return {
|
return {
|
||||||
initialImage,
|
isResetButtonDisabled: !initialImage,
|
||||||
useBatchAsInitialImage,
|
|
||||||
isResetButtonDisabled: useBatchAsInitialImage
|
|
||||||
? imageNames.length === 0
|
|
||||||
: !initialImage,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const postUploadAction: PostUploadAction = {
|
||||||
|
type: 'SET_INITIAL_IMAGE',
|
||||||
|
};
|
||||||
|
|
||||||
const InitialImageDisplay = () => {
|
const InitialImageDisplay = () => {
|
||||||
const { initialImage, useBatchAsInitialImage, isResetButtonDisabled } =
|
const { isResetButtonDisabled } = useAppSelector(selector);
|
||||||
useAppSelector(selector);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { openUploader } = useImageUploader();
|
const { openUploader } = useImageUploader();
|
||||||
|
|
||||||
const {
|
|
||||||
currentData: imageDTO,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
isSuccess,
|
|
||||||
} = useGetImageDTOQuery(initialImage?.imageName ?? skipToken);
|
|
||||||
|
|
||||||
const postUploadAction = useMemo<PostUploadAction>(
|
|
||||||
() =>
|
|
||||||
useBatchAsInitialImage
|
|
||||||
? { type: 'ADD_TO_BATCH' }
|
|
||||||
: { type: 'SET_INITIAL_IMAGE' },
|
|
||||||
[useBatchAsInitialImage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
if (useBatchAsInitialImage) {
|
dispatch(clearInitialImage());
|
||||||
dispatch(batchReset());
|
}, [dispatch]);
|
||||||
} else {
|
|
||||||
dispatch(clearInitialImage());
|
|
||||||
}
|
|
||||||
}, [dispatch, useBatchAsInitialImage]);
|
|
||||||
|
|
||||||
const handleUpload = useCallback(() => {
|
const handleUpload = useCallback(() => {
|
||||||
openUploader();
|
openUploader();
|
||||||
}, [openUploader]);
|
}, [openUploader]);
|
||||||
|
|
||||||
const handleClickUseBatch = useCallback(() => {
|
|
||||||
dispatch(asInitialImageToggled());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle={'first'}
|
layerStyle={'first'}
|
||||||
@ -114,40 +81,22 @@ const InitialImageDisplay = () => {
|
|||||||
Initial Image
|
Initial Image
|
||||||
</Text>
|
</Text>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{/* <IAIButton
|
|
||||||
tooltip={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
|
|
||||||
aria-label={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
|
|
||||||
leftIcon={<FaLayerGroup />}
|
|
||||||
isChecked={useBatchAsInitialImage}
|
|
||||||
onClick={handleClickUseBatch}
|
|
||||||
>
|
|
||||||
{useBatchAsInitialImage ? 'Batch' : 'Single'}
|
|
||||||
</IAIButton> */}
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={
|
tooltip={'Upload Initial Image'}
|
||||||
useBatchAsInitialImage ? 'Upload to Batch' : 'Upload Initial Image'
|
aria-label={'Upload Initial Image'}
|
||||||
}
|
|
||||||
aria-label={
|
|
||||||
useBatchAsInitialImage ? 'Upload to Batch' : 'Upload Initial Image'
|
|
||||||
}
|
|
||||||
icon={<FaUpload />}
|
icon={<FaUpload />}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
{...getUploadButtonProps()}
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={
|
tooltip={'Reset Initial Image'}
|
||||||
useBatchAsInitialImage ? 'Reset Batch' : 'Reset Initial Image'
|
aria-label={'Reset Initial Image'}
|
||||||
}
|
|
||||||
aria-label={
|
|
||||||
useBatchAsInitialImage ? 'Reset Batch' : 'Reset Initial Image'
|
|
||||||
}
|
|
||||||
icon={<FaUndo />}
|
icon={<FaUndo />}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
isDisabled={isResetButtonDisabled}
|
isDisabled={isResetButtonDisabled}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InitialImage />
|
<InitialImage />
|
||||||
{/* {useBatchAsInitialImage ? <BatchImageContainer /> : <InitialImage />} */}
|
|
||||||
<input {...getUploadInputProps()} />
|
<input {...getUploadInputProps()} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { merge } from 'lodash-es';
|
|||||||
export const initialConfigState: AppConfig = {
|
export const initialConfigState: AppConfig = {
|
||||||
shouldUpdateImagesOnConnect: false,
|
shouldUpdateImagesOnConnect: false,
|
||||||
disabledTabs: [],
|
disabledTabs: [],
|
||||||
disabledFeatures: ['lightbox', 'faceRestore'],
|
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
||||||
disabledSDFeatures: [
|
disabledSDFeatures: [
|
||||||
'variation',
|
'variation',
|
||||||
'seamless',
|
'seamless',
|
||||||
|
@ -15,7 +15,6 @@ import { RootState } from 'app/store/store';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
|
||||||
import { configSelector } from 'features/system/store/configSelectors';
|
import { configSelector } from 'features/system/store/configSelectors';
|
||||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
|
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
|
||||||
@ -38,7 +37,6 @@ import NodesTab from './tabs/Nodes/NodesTab';
|
|||||||
import ResizeHandle from './tabs/ResizeHandle';
|
import ResizeHandle from './tabs/ResizeHandle';
|
||||||
import TextToImageTab from './tabs/TextToImage/TextToImageTab';
|
import TextToImageTab from './tabs/TextToImage/TextToImageTab';
|
||||||
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
|
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
|
||||||
import { useFeatureStatus } from '../../system/hooks/useFeatureStatus';
|
|
||||||
|
|
||||||
export interface InvokeTabInfo {
|
export interface InvokeTabInfo {
|
||||||
id: InvokeTabName;
|
id: InvokeTabName;
|
||||||
@ -105,10 +103,6 @@ const InvokeTabs = () => {
|
|||||||
const activeTab = useAppSelector(activeTabIndexSelector);
|
const activeTab = useAppSelector(activeTabIndexSelector);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const enabledTabs = useAppSelector(enabledTabsSelector);
|
const enabledTabs = useAppSelector(enabledTabsSelector);
|
||||||
const isLightBoxOpen = useAppSelector(
|
|
||||||
(state: RootState) => state.lightbox.isLightboxOpen
|
|
||||||
);
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
|
||||||
|
|
||||||
const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } =
|
const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } =
|
||||||
useAppSelector((state: RootState) => state.ui);
|
useAppSelector((state: RootState) => state.ui);
|
||||||
@ -117,17 +111,6 @@ const InvokeTabs = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Lightbox Hotkey
|
|
||||||
useHotkeys(
|
|
||||||
'z',
|
|
||||||
() => {
|
|
||||||
if (isLightboxEnabled) {
|
|
||||||
dispatch(setIsLightboxOpen(!isLightBoxOpen));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isLightBoxOpen]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'f',
|
'f',
|
||||||
() => {
|
() => {
|
||||||
|
@ -2,7 +2,6 @@ import { Flex } from '@chakra-ui/react';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
import {
|
import {
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
@ -12,19 +11,16 @@ import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
||||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
import PinParametersPanelButton from './PinParametersPanelButton';
|
||||||
import OverlayScrollable from './common/OverlayScrollable';
|
|
||||||
import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer';
|
import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer';
|
||||||
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
||||||
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
||||||
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[uiSelector, activeTabNameSelector, lightboxSelector],
|
[uiSelector, activeTabNameSelector],
|
||||||
(ui, activeTabName, lightbox) => {
|
(ui, activeTabName) => {
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
||||||
|
|
||||||
const { isLightboxOpen } = lightbox;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTabName,
|
activeTabName,
|
||||||
shouldPinParametersPanel,
|
shouldPinParametersPanel,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||||
|
|
||||||
const TextToImageTabMain = () => {
|
const TextToImageTabMain = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
||||||
import { api } from '..';
|
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||||
import { paths } from '../schema';
|
import { paths } from '../schema';
|
||||||
import { imagesApi } from './images';
|
|
||||||
|
|
||||||
type ListBoardImagesArg =
|
type ListBoardImagesArg =
|
||||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
||||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
|
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
|
||||||
|
|
||||||
type AddImageToBoardArg =
|
type AddImageToBoardArg =
|
||||||
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
|
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
|
||||||
@ -25,9 +24,25 @@ export const boardImagesApi = api.injectEndpoints({
|
|||||||
>({
|
>({
|
||||||
query: ({ board_id, offset, limit }) => ({
|
query: ({ board_id, offset, limit }) => ({
|
||||||
url: `board_images/${board_id}`,
|
url: `board_images/${board_id}`,
|
||||||
method: 'DELETE',
|
method: 'GET',
|
||||||
body: { offset, limit },
|
|
||||||
}),
|
}),
|
||||||
|
providesTags: (result, error, arg) => {
|
||||||
|
// any list of boardimages
|
||||||
|
const tags: ApiFullTagDescription[] = [{ id: 'BoardImage', type: `${arg.board_id}_${LIST_TAG}` }];
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// and individual tags for each boardimage
|
||||||
|
tags.push(
|
||||||
|
...result.items.map(({ board_id, image_name }) => ({
|
||||||
|
type: 'BoardImage' as const,
|
||||||
|
id: `${board_id}_${image_name}`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,23 +56,9 @@ export const boardImagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, arg) => [
|
||||||
{ type: 'Board', id: arg.board_id },
|
{ type: 'BoardImage' },
|
||||||
|
{ type: 'Board', id: arg.board_id }
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
|
||||||
{ image_name, ...patch },
|
|
||||||
{ dispatch, queryFulfilled }
|
|
||||||
) {
|
|
||||||
const patchResult = dispatch(
|
|
||||||
imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => {
|
|
||||||
Object.assign(draft, patch);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await queryFulfilled;
|
|
||||||
} catch {
|
|
||||||
patchResult.undo();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
|
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
|
||||||
@ -67,23 +68,9 @@ export const boardImagesApi = api.injectEndpoints({
|
|||||||
body: { board_id, image_name },
|
body: { board_id, image_name },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, arg) => [
|
||||||
{ type: 'Board', id: arg.board_id },
|
{ type: 'BoardImage' },
|
||||||
|
{ type: 'Board', id: arg.board_id }
|
||||||
],
|
],
|
||||||
async onQueryStarted(
|
|
||||||
{ image_name, ...patch },
|
|
||||||
{ dispatch, queryFulfilled }
|
|
||||||
) {
|
|
||||||
const patchResult = dispatch(
|
|
||||||
imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => {
|
|
||||||
Object.assign(draft, { board_id: null });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await queryFulfilled;
|
|
||||||
} catch {
|
|
||||||
patchResult.undo();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||||
import { selectImagesAll } from 'features/gallery/store/gallerySlice';
|
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import {
|
||||||
|
ASSETS_CATEGORIES,
|
||||||
|
IMAGE_CATEGORIES,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { $client } from 'services/api/client';
|
import { $client } from 'services/api/client';
|
||||||
@ -287,15 +291,12 @@ export const receivedPageOfImages = createAppAsyncThunk<
|
|||||||
const { get } = $client.get();
|
const { get } = $client.get();
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { categories, selectedBoardId } = state.gallery;
|
|
||||||
|
|
||||||
const images = selectImagesAll(state).filter((i) => {
|
const images = selectFilteredImages(state);
|
||||||
const isInCategory = categories.includes(i.image_category);
|
const categories =
|
||||||
const isInSelectedBoard = selectedBoardId
|
state.gallery.galleryView === 'images'
|
||||||
? i.board_id === selectedBoardId
|
? IMAGE_CATEGORIES
|
||||||
: true;
|
: ASSETS_CATEGORIES;
|
||||||
return isInCategory && isInSelectedBoard;
|
|
||||||
});
|
|
||||||
|
|
||||||
let query: ListImagesArg = {};
|
let query: ListImagesArg = {};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user