mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-22 19:27:58 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efb187ced2 | ||
|
|
ca9150e9b3 |
@@ -72,7 +72,7 @@ async def upload_image(
|
||||
resize_to: Optional[str] = Body(
|
||||
default=None,
|
||||
description=f"Dimensions to resize the image to, must be stringified tuple of 2 integers. Max total pixel count: {ResizeToDimensions.MAX_SIZE}",
|
||||
examples=['"[1024,1024]"'],
|
||||
example='"[1024,1024]"',
|
||||
),
|
||||
metadata: Optional[str] = Body(
|
||||
default=None,
|
||||
|
||||
@@ -292,7 +292,7 @@ async def get_hugging_face_models(
|
||||
)
|
||||
async def update_model_record(
|
||||
key: Annotated[str, Path(description="Unique key of model")],
|
||||
changes: Annotated[ModelRecordChanges, Body(description="Model config", examples=[example_model_input])],
|
||||
changes: Annotated[ModelRecordChanges, Body(description="Model config", example=example_model_input)],
|
||||
) -> AnyModelConfig:
|
||||
"""Update a model's config."""
|
||||
logger = ApiDependencies.invoker.services.logger
|
||||
@@ -450,7 +450,7 @@ async def install_model(
|
||||
access_token: Optional[str] = Query(description="access token for the remote resource", default=None),
|
||||
config: ModelRecordChanges = Body(
|
||||
description="Object containing fields that override auto-probed values in the model config record, such as name, description and prediction_type ",
|
||||
examples=[{"name": "string", "description": "string"}],
|
||||
example={"name": "string", "description": "string"},
|
||||
),
|
||||
) -> ModelInstallJob:
|
||||
"""Install a model using a string identifier.
|
||||
|
||||
@@ -143,19 +143,11 @@ flux_dev = StarterModel(
|
||||
flux_kontext = StarterModel(
|
||||
name="FLUX.1 Kontext dev",
|
||||
base=BaseModelType.Flux,
|
||||
source="https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev/resolve/main/flux1-kontext-dev.safetensors",
|
||||
source="black-forest-labs/FLUX.1-Kontext-dev::flux1-kontext-dev.safetensors",
|
||||
description="FLUX.1 Kontext dev transformer in bfloat16. Total size with dependencies: ~33GB",
|
||||
type=ModelType.Main,
|
||||
dependencies=[t5_base_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
flux_kontext_quantized = StarterModel(
|
||||
name="FLUX.1 Kontext dev (Quantized)",
|
||||
base=BaseModelType.Flux,
|
||||
source="https://huggingface.co/unsloth/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_M.gguf",
|
||||
description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~14GB",
|
||||
type=ModelType.Main,
|
||||
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
sd35_medium = StarterModel(
|
||||
name="SD3.5 Medium",
|
||||
base=BaseModelType.StableDiffusion3,
|
||||
@@ -672,7 +664,7 @@ flux_fill = StarterModel(
|
||||
# List of starter models, displayed on the frontend.
|
||||
# The order/sort of this list is not changed by the frontend - set it how you want it here.
|
||||
STARTER_MODELS: list[StarterModel] = [
|
||||
flux_kontext_quantized,
|
||||
flux_kontext,
|
||||
flux_schnell_quantized,
|
||||
flux_dev_quantized,
|
||||
flux_schnell,
|
||||
@@ -793,7 +785,7 @@ flux_bundle: list[StarterModel] = [
|
||||
flux_depth_control_lora,
|
||||
flux_redux,
|
||||
flux_fill,
|
||||
flux_kontext_quantized,
|
||||
flux_kontext,
|
||||
]
|
||||
|
||||
STARTER_BUNDLES: dict[str, StarterModelBundle] = {
|
||||
|
||||
@@ -12,8 +12,6 @@ const config: KnipConfig = {
|
||||
'src/features/parameters/types/parameterSchemas.ts',
|
||||
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
|
||||
'src/features/controlLayers/konva/util.ts',
|
||||
// Will be using this
|
||||
'src/common/hooks/useAsyncState.ts',
|
||||
],
|
||||
ignoreBinaries: ['only-allow'],
|
||||
paths: {
|
||||
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks.",
|
||||
"imagenIncompatibleGenerationMode": "Google {{model}} supports Text to Image only. Use other models for Image to Image, Inpainting and Outpainting tasks.",
|
||||
"chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o supports Text to Image and Image to Image only. Use other models Inpainting and Outpainting tasks.",
|
||||
"fluxKontextIncompatibleGenerationMode": "FLUX Kontext does not support generation from images placed on the canvas. Re-try using the Reference Image section and disable any Raster Layers.",
|
||||
"fluxKontextIncompatibleGenerationMode": "FLUX Kontext supports Text to Image only. Use other models for Image to Image, Inpainting and Outpainting tasks.",
|
||||
"problemUnpublishingWorkflow": "Problem Unpublishing Workflow",
|
||||
"problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.",
|
||||
"workflowUnpublished": "Workflow Unpublished",
|
||||
@@ -2380,11 +2380,6 @@
|
||||
"saveToGallery": "Save To Gallery",
|
||||
"showResultsOn": "Showing Results",
|
||||
"showResultsOff": "Hiding Results"
|
||||
},
|
||||
"autoSwitch": {
|
||||
"off": "Off",
|
||||
"switchOnStart": "On Start",
|
||||
"switchOnFinish": "On Finish"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
@@ -2560,9 +2555,8 @@
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"items": [
|
||||
"Generate images faster with new Launchpads and a simplified Generate tab.",
|
||||
"Edit with prompts using Flux Kontext Dev.",
|
||||
"Export to PSD, bulk-hide overlays, organize models & images — all in a reimagined interface built for control."
|
||||
"Inpainting: Per-mask noise levels and denoise limits.",
|
||||
"Canvas: Smarter aspect ratios for SDXL and improved scroll-to-zoom."
|
||||
],
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchRecentReleaseVideos": "Watch Recent Release Videos",
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -30,64 +27,59 @@ GlobalImageHotkeys.displayName = 'GlobalImageHotkeys';
|
||||
const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const isViewerFocused = useIsRegionFocused('viewer');
|
||||
|
||||
const isFocusOK = isGalleryFocused || isViewerFocused;
|
||||
|
||||
const recallAll = useRecallAll(imageDTO);
|
||||
const recallRemix = useRecallRemix(imageDTO);
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
const recallDimensions = useRecallDimensions(imageDTO);
|
||||
const loadWorkflow = useLoadWorkflow(imageDTO);
|
||||
const imageActions = useImageActions(imageDTO);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'loadWorkflow',
|
||||
category: 'viewer',
|
||||
callback: loadWorkflow.load,
|
||||
options: { enabled: loadWorkflow.isEnabled && isFocusOK },
|
||||
dependencies: [loadWorkflow, isFocusOK],
|
||||
callback: imageActions.loadWorkflow,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.loadWorkflow, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallAll',
|
||||
category: 'viewer',
|
||||
callback: recallAll.recall,
|
||||
options: { enabled: recallAll.isEnabled && isFocusOK },
|
||||
dependencies: [recallAll, isFocusOK],
|
||||
callback: imageActions.recallAll,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallAll, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallSeed',
|
||||
category: 'viewer',
|
||||
callback: recallSeed.recall,
|
||||
options: { enabled: recallSeed.isEnabled && isFocusOK },
|
||||
dependencies: [recallSeed, isFocusOK],
|
||||
callback: imageActions.recallSeed,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallSeed, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallPrompts',
|
||||
category: 'viewer',
|
||||
callback: recallPrompts.recall,
|
||||
options: { enabled: recallPrompts.isEnabled && isFocusOK },
|
||||
dependencies: [recallPrompts, isFocusOK],
|
||||
callback: imageActions.recallPrompts,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallPrompts, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'remix',
|
||||
category: 'viewer',
|
||||
callback: recallRemix.recall,
|
||||
options: { enabled: recallRemix.isEnabled && isFocusOK },
|
||||
dependencies: [recallRemix, isFocusOK],
|
||||
callback: imageActions.remix,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.remix, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'useSize',
|
||||
category: 'viewer',
|
||||
callback: recallDimensions.recall,
|
||||
options: { enabled: recallDimensions.isEnabled && isFocusOK },
|
||||
dependencies: [recallDimensions, isFocusOK],
|
||||
callback: imageActions.recallSize,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallSize, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'runPostprocessing',
|
||||
category: 'viewer',
|
||||
callback: imageActions.upscale,
|
||||
options: { enabled: isUpscalingEnabled && isViewerFocused },
|
||||
dependencies: [isUpscalingEnabled, imageDTO, isViewerFocused],
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
|
||||
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
|
||||
import {
|
||||
selectAllEntitiesOfType,
|
||||
selectBboxModelBase,
|
||||
selectCanvasSlice,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { selectBboxModelBase } from 'features/controlLayers/store/selectors';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { selectGlobalRefImageModels, selectRegionalRefImageModels } from 'services/api/hooks/modelsByType';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
import {
|
||||
isChatGPT4oModelConfig,
|
||||
isFluxKontextApiModelConfig,
|
||||
isFluxKontextModelConfig,
|
||||
isFluxReduxModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
const log = logger('models');
|
||||
|
||||
@@ -39,8 +25,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
}
|
||||
|
||||
const newModel = result.data;
|
||||
const newBase = newModel.base;
|
||||
const didBaseModelChange = state.params.model?.base !== newBase;
|
||||
|
||||
const newBaseModel = newModel.base;
|
||||
const didBaseModelChange = state.params.model?.base !== newBaseModel;
|
||||
|
||||
if (didBaseModelChange) {
|
||||
// we may need to reset some incompatible submodels
|
||||
@@ -48,7 +35,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
|
||||
// handle incompatible loras
|
||||
state.loras.loras.forEach((lora) => {
|
||||
if (lora.model.base !== newBase) {
|
||||
if (lora.model.base !== newBaseModel) {
|
||||
dispatch(loraDeleted({ id: lora.id }));
|
||||
modelsCleared += 1;
|
||||
}
|
||||
@@ -56,82 +43,20 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
|
||||
// handle incompatible vae
|
||||
const { vae } = state.params;
|
||||
if (vae && vae.base !== newBase) {
|
||||
if (vae && vae.base !== newBaseModel) {
|
||||
dispatch(vaeSelected(null));
|
||||
modelsCleared += 1;
|
||||
}
|
||||
|
||||
// Handle incompatible reference image models - switch to first compatible model, with some smart logic
|
||||
// to choose the best available model based on the new main model.
|
||||
const allRefImageModels = selectGlobalRefImageModels(state).filter(({ base }) => base === newBase);
|
||||
|
||||
let newGlobalRefImageModel = null;
|
||||
|
||||
// Certain models require the ref image model to be the same as the main model - others just need a matching
|
||||
// base. Helper to grab the first exact match or the first available model if no exact match is found.
|
||||
const exactMatchOrFirst = <T extends AnyModelConfig>(candidates: T[]): T | null =>
|
||||
candidates.find(({ key }) => key === newModel.key) ?? candidates[0] ?? null;
|
||||
|
||||
// The only way we can differentiate between FLUX and FLUX Kontext is to check for "kontext" in the name
|
||||
if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) {
|
||||
const fluxKontextDevModels = allRefImageModels.filter(isFluxKontextModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextDevModels);
|
||||
} else if (newModel.base === 'chatgpt-4o') {
|
||||
const chatGPT4oModels = allRefImageModels.filter(isChatGPT4oModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(chatGPT4oModels);
|
||||
} else if (newModel.base === 'flux-kontext') {
|
||||
const fluxKontextApiModels = allRefImageModels.filter(isFluxKontextApiModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextApiModels);
|
||||
} else if (newModel.base === 'flux') {
|
||||
const fluxReduxModels = allRefImageModels.filter(isFluxReduxModelConfig);
|
||||
newGlobalRefImageModel = fluxReduxModels[0] ?? null;
|
||||
} else {
|
||||
newGlobalRefImageModel = allRefImageModels[0] ?? null;
|
||||
}
|
||||
|
||||
// All ref image entities are updated to use the same new model
|
||||
const refImageEntities = selectReferenceImageEntities(state);
|
||||
for (const entity of refImageEntities) {
|
||||
const shouldUpdateModel =
|
||||
(entity.config.model && entity.config.model.base !== newBase) ||
|
||||
(!entity.config.model && newGlobalRefImageModel);
|
||||
|
||||
if (shouldUpdateModel) {
|
||||
dispatch(
|
||||
refImageModelChanged({
|
||||
id: entity.id,
|
||||
modelConfig: newGlobalRefImageModel,
|
||||
})
|
||||
);
|
||||
modelsCleared += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For regional guidance, there is no smart logic - we just pick the first available model.
|
||||
const newRegionalRefImageModel = selectRegionalRefImageModels(state)[0] ?? null;
|
||||
|
||||
// All regional guidance entities are updated to use the same new model.
|
||||
const canvasState = selectCanvasSlice(state);
|
||||
const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance');
|
||||
for (const entity of canvasRegionalGuidanceEntities) {
|
||||
for (const refImage of entity.referenceImages) {
|
||||
// Only change the model if the current one is not compatible with the new base model.
|
||||
const shouldUpdateModel =
|
||||
(refImage.config.model && refImage.config.model.base !== newBase) ||
|
||||
(!refImage.config.model && newRegionalRefImageModel);
|
||||
|
||||
if (shouldUpdateModel) {
|
||||
dispatch(
|
||||
rgRefImageModelChanged({
|
||||
entityIdentifier: getEntityIdentifier(entity),
|
||||
referenceImageId: refImage.id,
|
||||
modelConfig: newRegionalRefImageModel,
|
||||
})
|
||||
);
|
||||
modelsCleared += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// handle incompatible controlnets
|
||||
// state.canvas.present.controlAdapters.entities.forEach((ca) => {
|
||||
// if (ca.model?.base !== newBaseModel) {
|
||||
// modelsCleared += 1;
|
||||
// if (ca.isEnabled) {
|
||||
// dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
if (modelsCleared > 0) {
|
||||
toast({
|
||||
|
||||
@@ -3,7 +3,6 @@ import { isNil } from 'es-toolkit';
|
||||
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import {
|
||||
heightChanged,
|
||||
setCfgRescaleMultiplier,
|
||||
setCfgScale,
|
||||
setGuidance,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
setSteps,
|
||||
vaePrecisionChanged,
|
||||
vaeSelected,
|
||||
widthChanged,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||
import {
|
||||
@@ -26,7 +24,6 @@ import {
|
||||
zParameterVAEModel,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { t } from 'i18next';
|
||||
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
@@ -116,24 +113,15 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
const setSizeOptions = { updateAspectRatio: true, clamp: true };
|
||||
|
||||
const isStaging = selectIsStaging(getState());
|
||||
const activeTab = selectActiveTab(getState());
|
||||
if (activeTab === 'generate') {
|
||||
if (!isStaging && width) {
|
||||
if (isParameterWidth(width)) {
|
||||
dispatch(widthChanged({ width, ...setSizeOptions }));
|
||||
}
|
||||
if (isParameterHeight(height)) {
|
||||
dispatch(heightChanged({ height, ...setSizeOptions }));
|
||||
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
|
||||
}
|
||||
}
|
||||
|
||||
if (activeTab === 'canvas') {
|
||||
if (!isStaging) {
|
||||
if (isParameterWidth(width)) {
|
||||
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
|
||||
}
|
||||
if (isParameterHeight(height)) {
|
||||
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
|
||||
}
|
||||
if (!isStaging && height) {
|
||||
if (isParameterHeight(height)) {
|
||||
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,14 @@ export const buildGroup = <T extends object>(group: Omit<Group<T>, typeof unique
|
||||
[uniqueGroupKey]: true,
|
||||
});
|
||||
|
||||
export const isGroup = <T extends object>(optionOrGroup: OptionOrGroup<T>): optionOrGroup is Group<T> => {
|
||||
const isGroup = <T extends object>(optionOrGroup: OptionOrGroup<T>): optionOrGroup is Group<T> => {
|
||||
return uniqueGroupKey in optionOrGroup && optionOrGroup[uniqueGroupKey] === true;
|
||||
};
|
||||
|
||||
export const isOption = <T extends object>(optionOrGroup: OptionOrGroup<T>): optionOrGroup is T => {
|
||||
return !(uniqueGroupKey in optionOrGroup);
|
||||
};
|
||||
|
||||
const DefaultOptionComponent = typedMemo(<T extends object>({ option }: { option: T }) => {
|
||||
const { getOptionId } = usePickerContext();
|
||||
return <Text fontWeight="bold">{getOptionId(option)}</Text>;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { WrappedError } from 'common/util/result';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type SuccessState<T> = {
|
||||
status: 'success';
|
||||
value: T;
|
||||
error: null;
|
||||
};
|
||||
|
||||
type ErrorState = {
|
||||
status: 'error';
|
||||
value: null;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
type PendingState = {
|
||||
status: 'pending';
|
||||
value: null;
|
||||
error: null;
|
||||
};
|
||||
|
||||
type IdleState = {
|
||||
status: 'idle';
|
||||
value: null;
|
||||
error: null;
|
||||
};
|
||||
|
||||
export type State<T> = IdleState | PendingState | SuccessState<T> | ErrorState;
|
||||
|
||||
type UseAsyncStateOptions = {
|
||||
immediate?: boolean;
|
||||
};
|
||||
|
||||
type UseAsyncReturn<T> = {
|
||||
$state: Atom<State<T>>;
|
||||
trigger: () => Promise<void>;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const useAsyncState = <T>(execute: () => Promise<T>, options?: UseAsyncStateOptions): UseAsyncReturn<T> => {
|
||||
const $state = useState(() =>
|
||||
atom<State<T>>({
|
||||
status: 'idle',
|
||||
value: null,
|
||||
error: null,
|
||||
})
|
||||
)[0];
|
||||
|
||||
const trigger = useCallback(async () => {
|
||||
$state.set({
|
||||
status: 'pending',
|
||||
value: null,
|
||||
error: null,
|
||||
});
|
||||
try {
|
||||
const value = await execute();
|
||||
$state.set({
|
||||
status: 'success',
|
||||
value,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
$state.set({
|
||||
status: 'error',
|
||||
value: null,
|
||||
error: WrappedError.wrap(error),
|
||||
});
|
||||
}
|
||||
}, [$state, execute]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
$state.set({
|
||||
status: 'idle',
|
||||
value: null,
|
||||
error: null,
|
||||
});
|
||||
}, [$state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (options?.immediate) {
|
||||
trigger();
|
||||
}
|
||||
}, [options?.immediate, trigger]);
|
||||
|
||||
const api = useMemo(
|
||||
() =>
|
||||
({
|
||||
$state,
|
||||
trigger,
|
||||
reset,
|
||||
}) satisfies UseAsyncReturn<T>,
|
||||
[$state, trigger, reset]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
type UseAsyncReturnReactive<T> = {
|
||||
state: State<T>;
|
||||
trigger: () => Promise<void>;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const useAsyncStateReactive = <T>(
|
||||
execute: () => Promise<T>,
|
||||
options?: UseAsyncStateOptions
|
||||
): UseAsyncReturnReactive<T> => {
|
||||
const { $state, trigger, reset } = useAsyncState(execute, options);
|
||||
const state = useStore($state);
|
||||
|
||||
return { state, trigger, reset };
|
||||
};
|
||||
@@ -61,7 +61,7 @@ export const RefImageImage = memo(
|
||||
)}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} borderRadius="base" borderWidth={1} borderStyle="solid" w="full" />
|
||||
<DndImage imageDTO={imageDTO} borderWidth={1} borderStyle="solid" w="full" />
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
|
||||
@@ -142,7 +142,6 @@ export const RefImagePreview = memo(() => {
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
cursor="pointer"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image
|
||||
src={imageDTO?.thumbnail_url}
|
||||
@@ -152,6 +151,7 @@ export const RefImagePreview = memo(() => {
|
||||
fallback={<Skeleton h="full" aspectRatio="1/1" />}
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
{isIPAdapterConfig(entity.config) && (
|
||||
<Flex
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
useCanvasSessionContext,
|
||||
useOutputImageDTO,
|
||||
@@ -11,10 +10,6 @@ import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession
|
||||
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
|
||||
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import {
|
||||
selectStagingAreaAutoSwitch,
|
||||
settingsStagingAreaAutoSwitchChanged,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -26,12 +21,12 @@ const sx = {
|
||||
pos: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
h: 108,
|
||||
w: 108,
|
||||
flexShrink: 0,
|
||||
aspectRatio: '1/1',
|
||||
borderWidth: 2,
|
||||
borderRadius: 'base',
|
||||
bg: 'base.900',
|
||||
overflow: 'hidden',
|
||||
'&[data-selected="true"]': {
|
||||
borderColor: 'invokeBlue.300',
|
||||
},
|
||||
@@ -44,24 +39,23 @@ type Props = {
|
||||
};
|
||||
|
||||
export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ctx = useCanvasSessionContext();
|
||||
const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
|
||||
const imageDTO = useOutputImageDTO(item);
|
||||
const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
ctx.$selectedItemId.set(item.item_id);
|
||||
}, [ctx.$selectedItemId, item.item_id]);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
const autoSwitch = ctx.$autoSwitch.get();
|
||||
if (autoSwitch !== 'off') {
|
||||
dispatch(settingsStagingAreaAutoSwitchChanged('off'));
|
||||
ctx.$autoSwitch.set('off');
|
||||
toast({
|
||||
title: 'Auto-Switch Disabled',
|
||||
});
|
||||
}
|
||||
}, [autoSwitch, dispatch]);
|
||||
}, [ctx.$autoSwitch]);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
ctx.onImageLoad(item.item_id);
|
||||
|
||||
@@ -16,21 +16,21 @@ export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
|
||||
|
||||
if (item.status === 'pending') {
|
||||
return (
|
||||
<Text fontSize="xs" pointerEvents="none" userSelect="none" fontWeight="semibold" color="base.300" {...rest}>
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="base.300" {...rest}>
|
||||
Pending
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (item.status === 'canceled') {
|
||||
return (
|
||||
<Text fontSize="xs" pointerEvents="none" userSelect="none" fontWeight="semibold" color="warning.300" {...rest}>
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="warning.300" {...rest}>
|
||||
Canceled
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (item.status === 'failed') {
|
||||
return (
|
||||
<Text fontSize="xs" pointerEvents="none" userSelect="none" fontWeight="semibold" color="error.300" {...rest}>
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="error.300" {...rest}>
|
||||
Failed
|
||||
</Text>
|
||||
);
|
||||
@@ -38,7 +38,7 @@ export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
|
||||
|
||||
if (item.status === 'in_progress') {
|
||||
return (
|
||||
<Text fontSize="xs" pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeBlue.300" {...rest}>
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeBlue.300" {...rest}>
|
||||
In Progress
|
||||
</Text>
|
||||
);
|
||||
@@ -46,14 +46,7 @@ export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
|
||||
|
||||
if (item.status === 'completed') {
|
||||
return (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
pointerEvents="none"
|
||||
userSelect="none"
|
||||
fontWeight="semibold"
|
||||
color="invokeGreen.300"
|
||||
{...rest}
|
||||
>
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeGreen.300" {...rest}>
|
||||
Completed
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -21,20 +21,18 @@ export const StagingAreaItemsList = memo(() => {
|
||||
}, [canvasManager, ctx.$progressData, ctx.$selectedItemId, ctx.$isPending]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" maxW="full" w="full" h="72px">
|
||||
<ScrollableContent overflowX="scroll" overflowY="hidden">
|
||||
<Flex gap={2} w="full" h="full" justifyContent="safe center">
|
||||
{items.map((item, i) => (
|
||||
<QueueItemPreviewMini
|
||||
key={`${item.item_id}-mini`}
|
||||
item={item}
|
||||
number={i + 1}
|
||||
isSelected={selectedItemId === item.item_id}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
<ScrollableContent overflowX="scroll" overflowY="hidden">
|
||||
<Flex gap={2} w="full" h="full" justifyContent="safe center">
|
||||
{items.map((item, i) => (
|
||||
<QueueItemPreviewMini
|
||||
key={`${item.item_id}-mini`}
|
||||
item={item}
|
||||
number={i + 1}
|
||||
isSelected={selectedItemId === item.item_id}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
);
|
||||
});
|
||||
StagingAreaItemsList.displayName = 'StagingAreaItemsList';
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { buildZodTypeGuard } from 'common/util/zodUtils';
|
||||
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { selectStagingAreaAutoSwitch } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
buildSelectSessionQueueItems,
|
||||
canvasQueueItemDiscarded,
|
||||
canvasSessionReset,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { canvasQueueItemDiscarded, selectDiscardedItems } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
|
||||
import { atom, computed, effect, map, subscribeKeys } from 'nanostores';
|
||||
@@ -17,6 +15,11 @@ import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import { $socket } from 'services/events/stores';
|
||||
import { assert, objectEntries } from 'tsafe';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']);
|
||||
export const isAutoSwitchMode = buildZodTypeGuard(zAutoSwitchMode);
|
||||
type AutoSwitchMode = z.infer<typeof zAutoSwitchMode>;
|
||||
|
||||
export type ProgressData = {
|
||||
itemId: number;
|
||||
@@ -96,13 +99,13 @@ type CanvasSessionContextValue = {
|
||||
$selectedItem: Atom<S['SessionQueueItem'] | null>;
|
||||
$selectedItemIndex: Atom<number | null>;
|
||||
$selectedItemOutputImageDTO: Atom<ImageDTO | null>;
|
||||
$autoSwitch: WritableAtom<AutoSwitchMode>;
|
||||
selectNext: () => void;
|
||||
selectPrev: () => void;
|
||||
selectFirst: () => void;
|
||||
selectLast: () => void;
|
||||
onImageLoad: (itemId: number) => void;
|
||||
discard: (itemId: number) => void;
|
||||
discardAll: () => void;
|
||||
};
|
||||
|
||||
const CanvasSessionContext = createContext<CanvasSessionContextValue | null>(null);
|
||||
@@ -139,6 +142,11 @@ export const CanvasSessionContextProvider = memo(
|
||||
*/
|
||||
const $items = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
|
||||
|
||||
/**
|
||||
* Whether auto-switch is enabled.
|
||||
*/
|
||||
const $autoSwitch = useState(() => atom<AutoSwitchMode>('switch_on_start'))[0];
|
||||
|
||||
/**
|
||||
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
|
||||
* output images have fully loaded.
|
||||
@@ -220,9 +228,25 @@ export const CanvasSessionContextProvider = memo(
|
||||
)[0];
|
||||
|
||||
/**
|
||||
* A redux selector to select all queue items from the RTK Query cache.
|
||||
* A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable
|
||||
* references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled
|
||||
* items) should be done in a nanostores computed.
|
||||
*/
|
||||
const selectQueueItems = useMemo(() => buildSelectSessionQueueItems(session.id), [session.id]);
|
||||
const selectQueueItems = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
[queueApi.endpoints.listAllQueueItems.select({ destination: session.id }), selectDiscardedItems],
|
||||
({ data }, discardedItems) => {
|
||||
if (!data) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
return data.filter(
|
||||
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
|
||||
);
|
||||
}
|
||||
),
|
||||
[session.id]
|
||||
);
|
||||
|
||||
const discard = useCallback(
|
||||
(itemId: number) => {
|
||||
@@ -231,10 +255,6 @@ export const CanvasSessionContextProvider = memo(
|
||||
[store]
|
||||
);
|
||||
|
||||
const discardAll = useCallback(() => {
|
||||
store.dispatch(canvasSessionReset());
|
||||
}, [store]);
|
||||
|
||||
const selectNext = useCallback(() => {
|
||||
const selectedItemId = $selectedItemId.get();
|
||||
if (selectedItemId === null) {
|
||||
@@ -296,15 +316,12 @@ export const CanvasSessionContextProvider = memo(
|
||||
imageLoaded: true,
|
||||
});
|
||||
}
|
||||
if (
|
||||
$lastCompletedItemId.get() === itemId &&
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish'
|
||||
) {
|
||||
if ($lastCompletedItemId.get() === itemId && $autoSwitch.get() === 'switch_on_finish') {
|
||||
$selectedItemId.set(itemId);
|
||||
$lastCompletedItemId.set(null);
|
||||
}
|
||||
},
|
||||
[$lastCompletedItemId, $progressData, $selectedItemId, store]
|
||||
[$autoSwitch, $lastCompletedItemId, $progressData, $selectedItemId]
|
||||
);
|
||||
|
||||
// Set up socket listeners
|
||||
@@ -339,7 +356,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
socket.off('invocation_progress', onProgress);
|
||||
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
};
|
||||
}, [$lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||
}, [$autoSwitch, $lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||
|
||||
// Set up state subscriptions and effects
|
||||
useEffect(() => {
|
||||
@@ -369,7 +386,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
$selectedItemId.set(items[0]?.item_id ?? null);
|
||||
return;
|
||||
} else if (
|
||||
selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_start' &&
|
||||
$autoSwitch.get() === 'switch_on_start' &&
|
||||
items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
|
||||
) {
|
||||
$selectedItemId.set(lastStartedItemId);
|
||||
@@ -472,7 +489,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
if (lastLoadedItemId === null) {
|
||||
return;
|
||||
}
|
||||
if (selectStagingAreaAutoSwitch(store.getState()) === 'switch_on_finish') {
|
||||
if ($autoSwitch.get() === 'switch_on_finish') {
|
||||
$selectedItemId.set(lastLoadedItemId);
|
||||
}
|
||||
$lastLoadedItemId.set(null);
|
||||
@@ -484,22 +501,6 @@ export const CanvasSessionContextProvider = memo(
|
||||
queueApi.endpoints.listAllQueueItems.initiate({ destination: session.id })
|
||||
);
|
||||
|
||||
// const unsubListener = store.dispatch(
|
||||
// addAppListener({
|
||||
// matcher: queueApi.endpoints.cancelQueueItem.matchFulfilled,
|
||||
// effect: ({ payload }, { getState }) => {
|
||||
// const { item_id } = payload;
|
||||
|
||||
// const items = selectQueueItems(getState());
|
||||
// if (items.length === 0) {
|
||||
// $selectedItemId.set(null);
|
||||
// } else if ($selectedItemId.get() === null) {
|
||||
// $selectedItemId.set(items[0].item_id);
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
|
||||
return () => {
|
||||
unsubHandleAutoSwitch();
|
||||
@@ -513,6 +514,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
};
|
||||
}, [
|
||||
$items,
|
||||
$autoSwitch,
|
||||
$lastLoadedItemId,
|
||||
$lastStartedItemId,
|
||||
$progressData,
|
||||
@@ -530,6 +532,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
$isPending,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
$autoSwitch,
|
||||
$selectedItem,
|
||||
$selectedItemIndex,
|
||||
$selectedItemOutputImageDTO,
|
||||
@@ -540,9 +543,9 @@ export const CanvasSessionContextProvider = memo(
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
}),
|
||||
[
|
||||
$autoSwitch,
|
||||
$items,
|
||||
$hasItems,
|
||||
$isPending,
|
||||
@@ -559,7 +562,6 @@ export const CanvasSessionContextProvider = memo(
|
||||
selectLast,
|
||||
onImageLoad,
|
||||
discard,
|
||||
discardAll,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectStagingAreaAutoSwitch,
|
||||
settingsStagingAreaAutoSwitchChanged,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiCaretLineRightBold, PiCaretRightBold, PiMoonBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaAutoSwitchButtons = memo(() => {
|
||||
const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onClickOff = useCallback(() => {
|
||||
dispatch(settingsStagingAreaAutoSwitchChanged('off'));
|
||||
}, [dispatch]);
|
||||
const onClickSwitchOnStart = useCallback(() => {
|
||||
dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_start'));
|
||||
}, [dispatch]);
|
||||
const onClickSwitchOnFinished = useCallback(() => {
|
||||
dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_finish'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Do not auto-switch"
|
||||
tooltip="Do not auto-switch"
|
||||
icon={<PiMoonBold />}
|
||||
colorScheme={autoSwitch === 'off' ? 'invokeBlue' : 'base'}
|
||||
onClick={onClickOff}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Switch on start"
|
||||
tooltip="Switch on start"
|
||||
icon={<PiCaretRightBold />}
|
||||
colorScheme={autoSwitch === 'switch_on_start' ? 'invokeBlue' : 'base'}
|
||||
onClick={onClickSwitchOnStart}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Switch on finish"
|
||||
tooltip="Switch on finish"
|
||||
icon={<PiCaretLineRightBold />}
|
||||
colorScheme={autoSwitch === 'switch_on_finish' ? 'invokeBlue' : 'base'}
|
||||
onClick={onClickSwitchOnFinished}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
StagingAreaAutoSwitchButtons.displayName = 'StagingAreaAutoSwitchButtons';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
@@ -15,8 +15,6 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerP
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { StagingAreaAutoSwitchButtons } from './StagingAreaAutoSwitchButtons';
|
||||
|
||||
export const StagingAreaToolbar = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
@@ -26,18 +24,15 @@ export const StagingAreaToolbar = memo(() => {
|
||||
useEffect(() => {
|
||||
return ctx.$selectedItemId.listen((id) => {
|
||||
if (id !== null) {
|
||||
document
|
||||
.getElementById(getQueueItemElementId(id))
|
||||
?.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'auto' });
|
||||
document.getElementById(getQueueItemElementId(id))?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}, [ctx.$selectedItemId]);
|
||||
|
||||
useHotkeys('meta+left', ctx.selectFirst, { preventDefault: true });
|
||||
useHotkeys('meta+right', ctx.selectLast, { preventDefault: true });
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<StagingAreaToolbarPrevButton isDisabled={!shouldShowStagedImage} />
|
||||
<StagingAreaToolbarImageCountButton />
|
||||
@@ -49,14 +44,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
<StagingAreaToolbarSaveSelectedToGalleryButton />
|
||||
<StagingAreaToolbarMenu />
|
||||
<StagingAreaToolbarDiscardSelectedButton isDisabled={!shouldShowStagedImage} />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<StagingAreaAutoSwitchButtons />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<StagingAreaToolbarDiscardAllButton isDisabled={!shouldShowStagedImage} />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCancelQueueItemsByDestination } from 'features/queue/hooks/useCancelQueueItemsByDestination';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -7,13 +9,21 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const cancelQueueItemsByDestination = useCancelQueueItemsByDestination();
|
||||
|
||||
const discardAll = useCallback(() => {
|
||||
ctx.discardAll();
|
||||
cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
|
||||
}, [cancelQueueItemsByDestination, ctx]);
|
||||
if (ctx.$isPending.get()) {
|
||||
cancelQueueItemsByDestination.trigger(ctx.session.id, { withToast: false });
|
||||
}
|
||||
if (ctx.session.type === 'advanced') {
|
||||
dispatch(canvasSessionReset());
|
||||
} else {
|
||||
// ctx.session.type === 'simple'
|
||||
dispatch(generateSessionReset());
|
||||
}
|
||||
}, [cancelQueueItemsByDestination, ctx.$isPending, ctx.session.id, ctx.session.type, dispatch]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -22,6 +32,7 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={discardAll}
|
||||
colorScheme="error"
|
||||
fontSize={16}
|
||||
isDisabled={isDisabled || cancelQueueItemsByDestination.isDisabled}
|
||||
isLoading={cancelQueueItemsByDestination.isLoading}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ctx = useCanvasSessionContext();
|
||||
const cancelQueueItem = useCancelQueueItem();
|
||||
const selectedItemId = useStore(ctx.$selectedItemId);
|
||||
@@ -19,7 +22,16 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
|
||||
}
|
||||
ctx.discard(selectedItemId);
|
||||
await cancelQueueItem.trigger(selectedItemId, { withToast: false });
|
||||
}, [selectedItemId, ctx, cancelQueueItem]);
|
||||
const itemCount = ctx.$itemCount.get();
|
||||
if (itemCount <= 1) {
|
||||
if (ctx.session.type === 'advanced') {
|
||||
dispatch(canvasSessionReset());
|
||||
} else {
|
||||
// ctx.session.type === 'simple'
|
||||
dispatch(generateSessionReset());
|
||||
}
|
||||
}
|
||||
}, [selectedItemId, ctx, cancelQueueItem, dispatch]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -28,6 +40,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
|
||||
icon={<PiXBold />}
|
||||
onClick={discardSelected}
|
||||
colorScheme="invokeBlue"
|
||||
fontSize={16}
|
||||
isDisabled={selectedItemId === null || cancelQueueItem.isDisabled || isDisabled}
|
||||
isLoading={cancelQueueItem.isLoading}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Menu, MenuButton, MenuDivider, MenuList } from '@invoke-ai/ui-library';
|
||||
import { StagingAreaToolbarMenuAutoSwitch } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch';
|
||||
import { StagingAreaToolbarNewLayerFromImageMenuItems } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage';
|
||||
import { memo } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { PiDotsThreeBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarMenu = memo(() => {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeVerticalBold />} colorScheme="invokeBlue" />
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeBold />} colorScheme="invokeBlue" />
|
||||
<MenuList>
|
||||
<StagingAreaToolbarMenuAutoSwitch />
|
||||
<MenuDivider />
|
||||
<StagingAreaToolbarNewLayerFromImageMenuItems />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { isAutoSwitchMode, useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const autoSwitch = useStore(ctx.$autoSwitch);
|
||||
|
||||
const onChange = useCallback(
|
||||
(val: string | string[]) => {
|
||||
assert(isAutoSwitchMode(val));
|
||||
ctx.$autoSwitch.set(val);
|
||||
},
|
||||
[ctx.$autoSwitch]
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuOptionGroup value={autoSwitch} onChange={onChange} title="Auto-Switch" type="radio">
|
||||
<MenuItemOption value="off" closeOnSelect={false}>
|
||||
Off
|
||||
</MenuItemOption>
|
||||
<MenuItemOption value="switch_on_start" closeOnSelect={false}>
|
||||
Switch on Start
|
||||
</MenuItemOption>
|
||||
<MenuItemOption value="switch_on_finish" closeOnSelect={false}>
|
||||
Switch on Finish
|
||||
</MenuItemOption>
|
||||
</MenuOptionGroup>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarMenuAutoSwitch.displayName = 'StagingAreaToolbarMenuAutoSwitch';
|
||||
@@ -1,41 +1,38 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { zRgbaColor } from 'features/controlLayers/store/types';
|
||||
import { z } from 'zod/v4';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']);
|
||||
|
||||
const zCanvasSettingsState = z.object({
|
||||
type CanvasSettingsState = {
|
||||
/**
|
||||
* Whether to show HUD (Heads-Up Display) on the canvas.
|
||||
*/
|
||||
showHUD: z.boolean().default(true),
|
||||
showHUD: boolean;
|
||||
/**
|
||||
* Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to
|
||||
* the canvas bounds.
|
||||
*/
|
||||
clipToBbox: z.boolean().default(false),
|
||||
clipToBbox: boolean;
|
||||
/**
|
||||
* Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead.
|
||||
*/
|
||||
dynamicGrid: z.boolean().default(false),
|
||||
dynamicGrid: boolean;
|
||||
/**
|
||||
* Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel.
|
||||
*/
|
||||
invertScrollForToolWidth: z.boolean().default(false),
|
||||
invertScrollForToolWidth: boolean;
|
||||
/**
|
||||
* The width of the brush tool.
|
||||
*/
|
||||
brushWidth: z.int().gt(0).default(50),
|
||||
brushWidth: number;
|
||||
/**
|
||||
* The width of the eraser tool.
|
||||
*/
|
||||
eraserWidth: z.int().gt(0).default(50),
|
||||
eraserWidth: number;
|
||||
/**
|
||||
* The color to use when drawing lines or filling shapes.
|
||||
*/
|
||||
color: zRgbaColor.default({ r: 31, g: 160, b: 224, a: 1 }), // invokeBlue.500
|
||||
color: RgbaColor;
|
||||
/**
|
||||
* Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations.
|
||||
*
|
||||
@@ -43,61 +40,75 @@ const zCanvasSettingsState = z.object({
|
||||
*
|
||||
* When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited.
|
||||
*/
|
||||
outputOnlyMaskedRegions: z.boolean().default(true),
|
||||
outputOnlyMaskedRegions: boolean;
|
||||
/**
|
||||
* Whether to automatically process the operations like filtering and auto-masking.
|
||||
*/
|
||||
autoProcess: z.boolean().default(true),
|
||||
autoProcess: boolean;
|
||||
/**
|
||||
* The snap-to-grid setting for the canvas.
|
||||
*/
|
||||
snapToGrid: z.boolean().default(true),
|
||||
snapToGrid: boolean;
|
||||
/**
|
||||
* Whether to show progress on the canvas when generating images.
|
||||
*/
|
||||
showProgressOnCanvas: z.boolean().default(true),
|
||||
showProgressOnCanvas: boolean;
|
||||
/**
|
||||
* Whether to show the bounding box overlay on the canvas.
|
||||
*/
|
||||
bboxOverlay: z.boolean().default(false),
|
||||
bboxOverlay: boolean;
|
||||
/**
|
||||
* Whether to preserve the masked region instead of inpainting it.
|
||||
*/
|
||||
preserveMask: z.boolean().default(false),
|
||||
preserveMask: boolean;
|
||||
/**
|
||||
* Whether to show only raster layers while staging.
|
||||
*/
|
||||
isolatedStagingPreview: z.boolean().default(true),
|
||||
isolatedStagingPreview: boolean;
|
||||
/**
|
||||
* Whether to show only the selected layer while filtering, transforming, or doing other operations.
|
||||
*/
|
||||
isolatedLayerPreview: z.boolean().default(true),
|
||||
isolatedLayerPreview: boolean;
|
||||
/**
|
||||
* Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used.
|
||||
*/
|
||||
pressureSensitivity: z.boolean().default(true),
|
||||
pressureSensitivity: boolean;
|
||||
/**
|
||||
* Whether to show the rule of thirds composition guide overlay on the canvas.
|
||||
*/
|
||||
ruleOfThirds: z.boolean().default(false),
|
||||
ruleOfThirds: boolean;
|
||||
/**
|
||||
* Whether to save all staging images to the gallery instead of keeping them as intermediate images.
|
||||
*/
|
||||
saveAllImagesToGallery: z.boolean().default(false),
|
||||
/**
|
||||
* The auto-switch mode for the canvas staging area.
|
||||
*/
|
||||
stagingAreaAutoSwitch: zAutoSwitchMode.default('switch_on_start'),
|
||||
});
|
||||
saveAllImagesToGallery: boolean;
|
||||
};
|
||||
|
||||
type CanvasSettingsState = z.infer<typeof zCanvasSettingsState>;
|
||||
const getInitialState = () => zCanvasSettingsState.parse({});
|
||||
const initialState: CanvasSettingsState = {
|
||||
showHUD: true,
|
||||
clipToBbox: false,
|
||||
dynamicGrid: false,
|
||||
brushWidth: 50,
|
||||
eraserWidth: 50,
|
||||
invertScrollForToolWidth: false,
|
||||
color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
|
||||
outputOnlyMaskedRegions: true,
|
||||
autoProcess: true,
|
||||
snapToGrid: true,
|
||||
showProgressOnCanvas: true,
|
||||
bboxOverlay: false,
|
||||
preserveMask: false,
|
||||
isolatedStagingPreview: true,
|
||||
isolatedLayerPreview: true,
|
||||
pressureSensitivity: true,
|
||||
ruleOfThirds: false,
|
||||
saveAllImagesToGallery: false,
|
||||
};
|
||||
|
||||
export const canvasSettingsSlice = createSlice({
|
||||
name: 'canvasSettings',
|
||||
initialState: getInitialState(),
|
||||
initialState,
|
||||
reducers: {
|
||||
settingsClipToBboxChanged: (state, action: PayloadAction<CanvasSettingsState['clipToBbox']>) => {
|
||||
settingsClipToBboxChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.clipToBbox = action.payload;
|
||||
},
|
||||
settingsDynamicGridToggled: (state) => {
|
||||
@@ -106,19 +117,16 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsShowHUDToggled: (state) => {
|
||||
state.showHUD = !state.showHUD;
|
||||
},
|
||||
settingsBrushWidthChanged: (state, action: PayloadAction<CanvasSettingsState['brushWidth']>) => {
|
||||
settingsBrushWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.brushWidth = Math.round(action.payload);
|
||||
},
|
||||
settingsEraserWidthChanged: (state, action: PayloadAction<CanvasSettingsState['eraserWidth']>) => {
|
||||
settingsEraserWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.eraserWidth = Math.round(action.payload);
|
||||
},
|
||||
settingsColorChanged: (state, action: PayloadAction<CanvasSettingsState['color']>) => {
|
||||
settingsColorChanged: (state, action: PayloadAction<RgbaColor>) => {
|
||||
state.color = action.payload;
|
||||
},
|
||||
settingsInvertScrollForToolWidthChanged: (
|
||||
state,
|
||||
action: PayloadAction<CanvasSettingsState['invertScrollForToolWidth']>
|
||||
) => {
|
||||
settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.invertScrollForToolWidth = action.payload;
|
||||
},
|
||||
settingsOutputOnlyMaskedRegionsToggled: (state) => {
|
||||
@@ -154,12 +162,6 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsSaveAllImagesToGalleryToggled: (state) => {
|
||||
state.saveAllImagesToGallery = !state.saveAllImagesToGallery;
|
||||
},
|
||||
settingsStagingAreaAutoSwitchChanged: (
|
||||
state,
|
||||
action: PayloadAction<CanvasSettingsState['stagingAreaAutoSwitch']>
|
||||
) => {
|
||||
state.stagingAreaAutoSwitch = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -182,7 +184,6 @@ export const {
|
||||
settingsPressureSensitivityToggled,
|
||||
settingsRuleOfThirdsToggled,
|
||||
settingsSaveAllImagesToGalleryToggled,
|
||||
settingsStagingAreaAutoSwitchChanged,
|
||||
} = canvasSettingsSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -192,7 +193,7 @@ const migrate = (state: any): any => {
|
||||
|
||||
export const canvasSettingsPersistConfig: PersistConfig<CanvasSettingsState> = {
|
||||
name: canvasSettingsSlice.name,
|
||||
initialState: getInitialState(),
|
||||
initialState,
|
||||
migrate,
|
||||
persistDenylist: [],
|
||||
};
|
||||
@@ -218,4 +219,3 @@ export const selectIsolatedLayerPreview = createCanvasSettingsSelector((settings
|
||||
export const selectPressureSensitivity = createCanvasSettingsSelector((settings) => settings.pressureSensitivity);
|
||||
export const selectRuleOfThirds = createCanvasSettingsSelector((settings) => settings.ruleOfThirds);
|
||||
export const selectSaveAllImagesToGallery = createCanvasSettingsSelector((settings) => settings.saveAllImagesToGallery);
|
||||
export const selectStagingAreaAutoSwitch = createCanvasSettingsSelector((settings) => settings.stagingAreaAutoSwitch);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
generateSessionId: string | null;
|
||||
@@ -80,34 +78,8 @@ export const selectGenerateSessionId = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
({ generateSessionId }) => generateSessionId
|
||||
);
|
||||
export const buildSelectSessionQueueItems = (sessionId: string) =>
|
||||
createSelector(
|
||||
[queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems],
|
||||
({ data }, discardedItems) => {
|
||||
if (!data) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
return data.filter(
|
||||
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const selectIsStaging = (state: RootState) => {
|
||||
const sessionId = selectCanvasSessionId(state);
|
||||
if (!sessionId) {
|
||||
return false;
|
||||
}
|
||||
const { data } = queueApi.endpoints.listAllQueueItems.select({ destination: sessionId })(state);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const discardedItems = selectDiscardedItems(state);
|
||||
return data.some(
|
||||
({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id)
|
||||
);
|
||||
};
|
||||
const selectDiscardedItems = createSelector(
|
||||
export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null);
|
||||
export const selectDiscardedItems = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems
|
||||
);
|
||||
|
||||
@@ -98,7 +98,7 @@ const zRgbColor = z.object({
|
||||
b: z.number().int().min(0).max(255),
|
||||
});
|
||||
export type RgbColor = z.infer<typeof zRgbColor>;
|
||||
export const zRgbaColor = zRgbColor.extend({
|
||||
const zRgbaColor = zRgbColor.extend({
|
||||
a: z.number().min(0).max(1),
|
||||
});
|
||||
export type RgbaColor = z.infer<typeof zRgbaColor>;
|
||||
|
||||
@@ -15,6 +15,7 @@ const sx = {
|
||||
objectFit: 'contain',
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
cursor: 'grab',
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowBendUpLeftBold,
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiPaintBrushBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const subMenu = useSubMenu();
|
||||
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const recallAll = useRecallAll(imageDTO);
|
||||
const recallRemix = useRecallRemix(imageDTO);
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
const recallDimensions = useRecallDimensions(imageDTO);
|
||||
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } =
|
||||
useImageActions(imageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
@@ -36,24 +28,20 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
<SubMenuButtonContent label={t('parameters.recallMetadata')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={recallRemix.recall}
|
||||
isDisabled={!recallRemix.isEnabled}
|
||||
>
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={remix} isDisabled={!hasMetadata}>
|
||||
{t('parameters.remixImage')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts.recall} isDisabled={!recallPrompts.isEnabled}>
|
||||
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts} isDisabled={!hasPrompts}>
|
||||
{t('parameters.usePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlantBold />} onClick={recallSeed.recall} isDisabled={!recallSeed.isEnabled}>
|
||||
<MenuItem icon={<PiPlantBold />} onClick={recallSeed} isDisabled={!hasSeed}>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll.recall} isDisabled={!recallAll.isEnabled}>
|
||||
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll} isDisabled={!hasMetadata}>
|
||||
{t('parameters.useAll')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiRulerBold />} onClick={recallDimensions.recall} isDisabled={!recallDimensions.isEnabled}>
|
||||
{t('parameters.useSize')}
|
||||
<MenuItem icon={<PiPaintBrushBold />} onClick={createAsPreset} isDisabled={!hasPrompts}>
|
||||
{t('stylePresets.useForTemplate')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useCreateStylePresetFromMetadata } from 'features/gallery/hooks/useCreateStylePresetFromMetadata';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPaintBrushBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemUseAsPromptTemplate = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const stylePreset = useCreateStylePresetFromMetadata(imageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiPaintBrushBold />} onClickCapture={stylePreset.create} isDisabled={!stylePreset.isEnabled}>
|
||||
{t('stylePresets.useForTemplate')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemUseAsPromptTemplate.displayName = 'ImageMenuItemUseAsPromptTemplate';
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
|
||||
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
|
||||
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
|
||||
@@ -17,19 +16,14 @@ import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContex
|
||||
import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage';
|
||||
import { ImageMenuItemUseForPromptGeneration } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseForPromptGeneration';
|
||||
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { ImageMenuItemUseAsPromptTemplate } from './ImageMenuItemUseAsPromptTemplate';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<ImageDTOContextProvider value={imageDTO}>
|
||||
<IconMenuItemGroup>
|
||||
@@ -42,14 +36,13 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
|
||||
</IconMenuItemGroup>
|
||||
<MenuDivider />
|
||||
<ImageMenuItemLoadWorkflow />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemMetadataRecallActions />}
|
||||
<ImageMenuItemMetadataRecallActions />
|
||||
<MenuDivider />
|
||||
<ImageMenuItemSendToUpscale />
|
||||
<ImageMenuItemUseForPromptGeneration />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemUseAsRefImage />}
|
||||
<ImageMenuItemUseAsPromptTemplate />
|
||||
<ImageMenuItemUseAsRefImage />
|
||||
<ImageMenuItemNewCanvasFromImageSubMenu />
|
||||
{tab === 'canvas' && <ImageMenuItemNewLayerFromImageSubMenu />}
|
||||
<ImageMenuItemNewLayerFromImageSubMenu />
|
||||
<MenuDivider />
|
||||
<ImageMenuItemChangeBoard />
|
||||
<ImageMenuItemStarUnstar />
|
||||
|
||||
@@ -85,7 +85,7 @@ const UnrecallableMetadataParsed = typedMemo(
|
||||
|
||||
return (
|
||||
<Box as="span" lineHeight={1}>
|
||||
<LabelComponent i18nKey={handler.i18nKey} />
|
||||
<LabelComponent />
|
||||
<ValueComponent value={data.value} />
|
||||
</Box>
|
||||
);
|
||||
@@ -128,7 +128,7 @@ const SingleMetadataParsed = typedMemo(
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Box as="span" lineHeight={1}>
|
||||
<LabelComponent i18nKey={handler.i18nKey} />
|
||||
<LabelComponent />
|
||||
<ValueComponent value={data.value} />
|
||||
</Box>
|
||||
</Flex>
|
||||
@@ -178,7 +178,7 @@ const CollectionMetadataParsed = typedMemo(
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Box as="span" lineHeight={1}>
|
||||
<LabelComponent i18nKey={handler.i18nKey} />
|
||||
<LabelComponent />
|
||||
<ValueComponent value={value} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage';
|
||||
import { useEditImage } from 'features/gallery/hooks/useEditImage';
|
||||
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
@@ -25,23 +27,51 @@ import {
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
|
||||
export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
|
||||
import { useImageViewerContext } from './context';
|
||||
|
||||
export const CurrentImageButtons = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isCanvasOrGenerateTab = tab === 'canvas' || tab === 'generate';
|
||||
const ctx = useImageViewerContext();
|
||||
const hasProgressImage = useStore(ctx.$hasProgressImage);
|
||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||
const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer;
|
||||
|
||||
const imageName = useAppSelector(selectLastSelectedImage);
|
||||
const imageDTO = useImageDTO(imageName);
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
const imageActions = useImageActions(imageDTO);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
|
||||
const recallAll = useRecallAll(imageDTO);
|
||||
const recallRemix = useRecallRemix(imageDTO);
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
const recallDimensions = useRecallDimensions(imageDTO);
|
||||
const loadWorkflow = useLoadWorkflow(imageDTO);
|
||||
const editImage = useEditImage(imageDTO);
|
||||
const deleteImage = useDeleteImage(imageDTO);
|
||||
const handleEdit = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
|
||||
await newCanvasFromImage({
|
||||
imageDTO,
|
||||
type: 'raster_layer',
|
||||
withInpaintMask: true,
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
|
||||
// Automatically select the brush tool when editing an image
|
||||
if (canvasManager) {
|
||||
canvasManager.tool.$tool.set('brush');
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, getState, dispatch, t, canvasManager]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -50,7 +80,7 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
as={IconButton}
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO}
|
||||
isDisabled={isDisabledOverride || !imageDTO}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
@@ -62,8 +92,8 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
|
||||
<Button
|
||||
leftIcon={<PiPencilBold />}
|
||||
onClick={editImage.edit}
|
||||
isDisabled={!editImage.isEnabled}
|
||||
onClick={handleEdit}
|
||||
isDisabled={isDisabledOverride || !imageDTO}
|
||||
variant="link"
|
||||
size="sm"
|
||||
alignSelf="stretch"
|
||||
@@ -78,72 +108,62 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!loadWorkflow.isEnabled}
|
||||
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasWorkflow || !hasTemplates}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={loadWorkflow.load}
|
||||
onClick={imageActions.loadWorkflow}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageActions.remix}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasPrompts}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageActions.recallPrompts}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasSeed}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageActions.recallSeed}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageActions.recallSize}
|
||||
isDisabled={isDisabledOverride || !imageDTO || isStaging}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={imageActions.recallAll}
|
||||
/>
|
||||
{isCanvasOrGenerateTab && (
|
||||
<IconButton
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={!recallRemix.isEnabled}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={recallRemix.recall}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
<IconButton
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!recallPrompts.isEnabled}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={recallPrompts.recall}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
<IconButton
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={!recallSeed.isEnabled}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={recallSeed.recall}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
<IconButton
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={recallDimensions.recall}
|
||||
isDisabled={!recallDimensions.isEnabled}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
<IconButton
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!recallAll.isEnabled}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={recallAll.recall}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} isDisabled={false} />}
|
||||
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} isDisabled={isDisabledOverride} />}
|
||||
|
||||
<Divider orientation="vertical" h={8} mx={2} />
|
||||
|
||||
<DeleteImageButton onClick={deleteImage.delete} isDisabled={!deleteImage.isEnabled} />
|
||||
<DeleteImageButton onClick={imageActions.delete} isDisabled={isDisabledOverride || !imageDTO} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
|
||||
>
|
||||
{imageDTO && (
|
||||
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
|
||||
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} borderRadius="base" />
|
||||
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} />
|
||||
</Flex>
|
||||
)}
|
||||
{!imageDTO && <NoContentForViewer />}
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
|
||||
import { CurrentImageButtons } from './CurrentImageButtons';
|
||||
import { ToggleProgressButton } from './ToggleProgressButton';
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const imageName = useAppSelector(selectLastSelectedImage);
|
||||
const imageDTO = useImageDTO(imageName);
|
||||
|
||||
return (
|
||||
<Flex w="full" justifyContent="center" h={8}>
|
||||
<ToggleProgressButton />
|
||||
<Spacer />
|
||||
{imageDTO && <CurrentImageButtons imageDTO={imageDTO} />}
|
||||
<CurrentImageButtons />
|
||||
<Spacer />
|
||||
{imageDTO && <ToggleMetadataViewerButton />}
|
||||
<ToggleMetadataViewerButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Box, Flex, forwardRef, Grid, GridItem, Spinner, Text } from '@invoke-ai
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { getFocusedRegion } from 'common/hooks/focus';
|
||||
import { useRangeBasedImageFetching } from 'features/gallery/hooks/useRangeBasedImageFetching';
|
||||
import type { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
@@ -222,10 +221,6 @@ const useKeyboardNavigation = (
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (getFocusedRegion() !== 'gallery') {
|
||||
// Only handle keyboard navigation when the gallery is focused
|
||||
return;
|
||||
}
|
||||
// Only handle arrow keys
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
return;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import {
|
||||
activeStylePresetIdChanged,
|
||||
selectStylePresetActivePresetId,
|
||||
} from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useClearStylePresetWithToast = () => {
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
|
||||
const clearStylePreset = useCallback(() => {
|
||||
if (activeStylePresetId) {
|
||||
store.dispatch(activeStylePresetIdChanged(null));
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('stylePresets.promptTemplateCleared'),
|
||||
});
|
||||
}
|
||||
}, [activeStylePresetId, store, t]);
|
||||
|
||||
return clearStylePreset;
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useCreateStylePresetFromMetadata = (imageDTO?: ImageDTO | null) => {
|
||||
const store = useAppStore();
|
||||
const [hasPrompts, setHasPrompts] = useState(false);
|
||||
|
||||
const { metadata } = useDebouncedMetadata(imageDTO?.image_name);
|
||||
|
||||
useEffect(() => {
|
||||
MetadataUtils.hasMetadataByHandlers({
|
||||
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
|
||||
metadata,
|
||||
store,
|
||||
require: 'some',
|
||||
})
|
||||
.then((result) => {
|
||||
setHasPrompts(result);
|
||||
})
|
||||
.catch(() => {
|
||||
setHasPrompts(false);
|
||||
});
|
||||
}, [metadata, store]);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!imageDTO) {
|
||||
return false;
|
||||
}
|
||||
if (!hasPrompts) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [hasPrompts, imageDTO]);
|
||||
|
||||
const create = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let positivePrompt: string;
|
||||
let negativePrompt: string;
|
||||
|
||||
try {
|
||||
positivePrompt = await MetadataHandlers.PositivePrompt.parse(metadata, store);
|
||||
} catch (error) {
|
||||
positivePrompt = '';
|
||||
}
|
||||
try {
|
||||
negativePrompt = (await MetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
|
||||
} catch (error) {
|
||||
negativePrompt = '';
|
||||
}
|
||||
|
||||
$stylePresetModalState.set({
|
||||
prefilledFormData: {
|
||||
name: '',
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
imageUrl: imageDTO.image_url,
|
||||
type: 'user',
|
||||
},
|
||||
updatingStylePresetId: null,
|
||||
isModalOpen: true,
|
||||
});
|
||||
}, [imageDTO, isEnabled, metadata, store]);
|
||||
|
||||
return {
|
||||
create,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useDeleteImage = (imageDTO?: ImageDTO | null) => {
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}, [imageDTO]);
|
||||
const _delete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([imageDTO.image_name]);
|
||||
}, [deleteImageModal, imageDTO, isEnabled]);
|
||||
|
||||
return {
|
||||
delete: _delete,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useEditImage = (imageDTO?: ImageDTO | null) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!imageDTO) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [imageDTO]);
|
||||
|
||||
const edit = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await newCanvasFromImage({
|
||||
imageDTO,
|
||||
type: 'raster_layer',
|
||||
withInpaintMask: true,
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
|
||||
if (canvasManager) {
|
||||
canvasManager.tool.$tool.set('brush');
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, isEnabled, getState, dispatch, canvasManager, t]);
|
||||
|
||||
return {
|
||||
edit,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import {
|
||||
activeStylePresetIdChanged,
|
||||
selectStylePresetActivePresetId,
|
||||
} from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const { metadata } = useDebouncedMetadata(imageDTO?.image_name ?? null);
|
||||
const [hasMetadata, setHasMetadata] = useState(false);
|
||||
const [hasSeed, setHasSeed] = useState(false);
|
||||
const [hasPrompts, setHasPrompts] = useState(false);
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
useEffect(() => {
|
||||
const parseMetadata = async () => {
|
||||
if (metadata) {
|
||||
setHasMetadata(true);
|
||||
try {
|
||||
await MetadataHandlers.Seed.parse(metadata, store);
|
||||
setHasSeed(true);
|
||||
} catch {
|
||||
setHasSeed(false);
|
||||
}
|
||||
|
||||
let hasPrompt = false;
|
||||
// Need to catch all of these to avoid unhandled promise rejections bubbling up to instrumented error handlers
|
||||
for (const handler of [
|
||||
MetadataHandlers.PositivePrompt,
|
||||
MetadataHandlers.NegativePrompt,
|
||||
MetadataHandlers.PositiveStylePrompt,
|
||||
MetadataHandlers.NegativeStylePrompt,
|
||||
]) {
|
||||
try {
|
||||
await handler.parse(metadata, store);
|
||||
hasPrompt = true;
|
||||
break;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
setHasPrompts(hasPrompt);
|
||||
} else {
|
||||
setHasMetadata(false);
|
||||
setHasSeed(false);
|
||||
setHasPrompts(false);
|
||||
}
|
||||
};
|
||||
parseMetadata();
|
||||
}, [metadata, store]);
|
||||
|
||||
const clearStylePreset = useCallback(() => {
|
||||
if (activeStylePresetId) {
|
||||
store.dispatch(activeStylePresetIdChanged(null));
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('stylePresets.promptTemplateCleared'),
|
||||
});
|
||||
}
|
||||
}, [activeStylePresetId, store, t]);
|
||||
|
||||
const recallAll = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallAll(metadata, store, isStaging ? [MetadataHandlers.Width, MetadataHandlers.Height] : []);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, store, isStaging, clearStylePreset]);
|
||||
|
||||
const remix = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
// Recalls all metadata parameters except seed
|
||||
MetadataUtils.recallAll(metadata, store, [MetadataHandlers.Seed]);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, store, clearStylePreset]);
|
||||
|
||||
const recallSeed = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallByHandler({ metadata, store, handler: MetadataHandlers.Seed });
|
||||
}, [imageDTO, metadata, store]);
|
||||
|
||||
const recallPrompts = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallPrompts(metadata, store);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, store, clearStylePreset]);
|
||||
|
||||
const createAsPreset = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
let positivePrompt: string;
|
||||
let negativePrompt: string;
|
||||
|
||||
try {
|
||||
positivePrompt = await MetadataHandlers.PositivePrompt.parse(metadata, store);
|
||||
} catch (error) {
|
||||
positivePrompt = '';
|
||||
}
|
||||
try {
|
||||
negativePrompt = (await MetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
|
||||
} catch (error) {
|
||||
negativePrompt = '';
|
||||
}
|
||||
|
||||
$stylePresetModalState.set({
|
||||
prefilledFormData: {
|
||||
name: '',
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
imageUrl: imageDTO.image_url,
|
||||
type: 'user',
|
||||
},
|
||||
updatingStylePresetId: null,
|
||||
isModalOpen: true,
|
||||
});
|
||||
}, [imageDTO, metadata, store]);
|
||||
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
|
||||
const loadWorkflowFromImage = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!imageDTO.has_workflow || !hasTemplates) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
|
||||
}, [hasTemplates, imageDTO, loadWorkflowWithDialog]);
|
||||
|
||||
const recallSize = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (isStaging) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallDimensions(imageDTO, store);
|
||||
}, [imageDTO, isStaging, store]);
|
||||
|
||||
const upscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
store.dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||
}, [imageDTO, store]);
|
||||
|
||||
const _delete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([imageDTO.image_name]);
|
||||
}, [deleteImageModal, imageDTO]);
|
||||
|
||||
return {
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
recallAll,
|
||||
remix,
|
||||
recallSeed,
|
||||
recallPrompts,
|
||||
createAsPreset,
|
||||
loadWorkflow: loadWorkflowFromImage,
|
||||
hasWorkflow: imageDTO?.has_workflow ?? false,
|
||||
recallSize,
|
||||
upscale,
|
||||
delete: _delete,
|
||||
};
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useLoadWorkflow = (imageDTO: ImageDTO) => {
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!imageDTO.has_workflow) {
|
||||
return false;
|
||||
}
|
||||
if (!hasTemplates) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [hasTemplates, imageDTO]);
|
||||
|
||||
const load = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
|
||||
}, [imageDTO, isEnabled, loadWorkflowWithDialog]);
|
||||
|
||||
return { load, isEnabled };
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ListRange } from 'react-virtuoso';
|
||||
import { imagesApi, useGetImageDTOsByNamesMutation } from 'services/api/endpoints/images';
|
||||
import { useThrottledCallback } from 'use-debounce';
|
||||
@@ -53,7 +53,6 @@ export const useRangeBasedImageFetching = ({
|
||||
}: UseRangeBasedImageFetchingArgs): UseRangeBasedImageFetchingReturn => {
|
||||
const store = useAppStore();
|
||||
const [getImageDTOsByNames] = useGetImageDTOsByNamesMutation();
|
||||
const lastRangeRef = useRef<ListRange | null>(null);
|
||||
|
||||
const fetchImages = useCallback(
|
||||
(visibleRange: ListRange) => {
|
||||
@@ -66,7 +65,6 @@ export const useRangeBasedImageFetching = ({
|
||||
return;
|
||||
}
|
||||
getImageDTOsByNames({ image_names: uncachedNames });
|
||||
lastRangeRef.current = visibleRange;
|
||||
},
|
||||
[enabled, getImageDTOsByNames, imageNames, store]
|
||||
);
|
||||
@@ -80,13 +78,6 @@ export const useRangeBasedImageFetching = ({
|
||||
[throttledFetchImages]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastRangeRef.current) {
|
||||
return;
|
||||
}
|
||||
throttledFetchImages(lastRangeRef.current);
|
||||
}, [imageNames, throttledFetchImages]);
|
||||
|
||||
return {
|
||||
onRangeChanged,
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
|
||||
|
||||
export const useRecallAll = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const clearStylePreset = useClearStylePresetWithToast();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isLoading, metadata, tab]);
|
||||
|
||||
const handlersToSkip = useMemo(() => {
|
||||
if (tab === 'canvas' && isStaging) {
|
||||
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
|
||||
return [MetadataHandlers.Width, MetadataHandlers.Height];
|
||||
}
|
||||
return undefined;
|
||||
}, [isStaging, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallAll(metadata, store, handlersToSkip);
|
||||
clearStylePreset();
|
||||
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useRecallDimensions = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab === 'canvas' && isStaging) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isStaging, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallDimensions(imageDTO, store);
|
||||
}, [isEnabled, imageDTO, store]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
|
||||
|
||||
export const useRecallPrompts = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const clearStylePreset = useClearStylePresetWithToast();
|
||||
const [hasPrompts, setHasPrompts] = useState(false);
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
|
||||
useEffect(() => {
|
||||
const parse = async () => {
|
||||
try {
|
||||
const result = await MetadataUtils.hasMetadataByHandlers({
|
||||
handlers: [
|
||||
MetadataHandlers.PositivePrompt,
|
||||
MetadataHandlers.NegativePrompt,
|
||||
MetadataHandlers.PositiveStylePrompt,
|
||||
MetadataHandlers.NegativeStylePrompt,
|
||||
],
|
||||
metadata,
|
||||
store,
|
||||
require: 'some',
|
||||
});
|
||||
setHasPrompts(result);
|
||||
} catch {
|
||||
setHasPrompts(false);
|
||||
}
|
||||
};
|
||||
|
||||
parse();
|
||||
}, [metadata, store]);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasPrompts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [hasPrompts, isLoading, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallPrompts(metadata, store);
|
||||
clearStylePreset();
|
||||
}, [metadata, isEnabled, store, clearStylePreset]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
|
||||
|
||||
export const useRecallRemix = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const clearStylePreset = useClearStylePresetWithToast();
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isLoading, metadata, tab]);
|
||||
|
||||
const handlersToSkip = useMemo(() => {
|
||||
// Remix always skips the seed handler
|
||||
const _handlersToSkip = [MetadataHandlers.Seed];
|
||||
if (tab === 'canvas' && isStaging) {
|
||||
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
|
||||
_handlersToSkip.push(MetadataHandlers.Width, MetadataHandlers.Height);
|
||||
}
|
||||
return _handlersToSkip;
|
||||
}, [isStaging, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallAll(metadata, store, handlersToSkip);
|
||||
clearStylePreset();
|
||||
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useRecallSeed = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const [hasSeed, setHasSeed] = useState(false);
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
|
||||
useEffect(() => {
|
||||
const parse = async () => {
|
||||
try {
|
||||
await MetadataHandlers.Seed.parse(metadata, store);
|
||||
setHasSeed(true);
|
||||
} catch {
|
||||
setHasSeed(false);
|
||||
}
|
||||
};
|
||||
|
||||
parse();
|
||||
}, [metadata, store]);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasSeed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [hasSeed, isLoading, metadata, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.Seed, store });
|
||||
}, [metadata, isEnabled, store]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { bboxHeightChanged, bboxWidthChanged, canvasMetadataRecalled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/lorasSlice';
|
||||
import {
|
||||
heightChanged,
|
||||
negativePrompt2Changed,
|
||||
negativePromptChanged,
|
||||
positivePrompt2Changed,
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
setSteps,
|
||||
shouldConcatPromptsChanged,
|
||||
vaeSelected,
|
||||
widthChanged,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { refImagesRecalled } from 'features/controlLayers/store/refImagesSlice';
|
||||
import type { CanvasMetadata, LoRA, RefImageState } from 'features/controlLayers/store/types';
|
||||
@@ -84,9 +82,8 @@ import {
|
||||
zParameterStrength,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { t } from 'i18next';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { modelsApi } from 'services/api/endpoints/models';
|
||||
@@ -173,8 +170,7 @@ export type SingleMetadataHandler<T> = {
|
||||
type: string;
|
||||
parse: (metadata: unknown, store: AppStore) => Promise<T>;
|
||||
recall: (value: T, store: AppStore) => void;
|
||||
i18nKey: string;
|
||||
LabelComponent: ComponentType<{ i18nKey: string }>;
|
||||
LabelComponent: ComponentType;
|
||||
ValueComponent: ComponentType<SingleMetadataValueProps<T>>;
|
||||
};
|
||||
|
||||
@@ -188,8 +184,7 @@ export type CollectionMetadataHandler<T extends any[]> = {
|
||||
parse: (metadata: unknown, store: AppStore) => Promise<T>;
|
||||
recall: (values: T, store: AppStore) => void;
|
||||
recallOne: (value: T[number], store: AppStore) => void;
|
||||
i18nKey: string;
|
||||
LabelComponent: ComponentType<{ i18nKey: string }>;
|
||||
LabelComponent: ComponentType;
|
||||
ValueComponent: ComponentType<CollectionMetadataValueProps<T>>;
|
||||
};
|
||||
|
||||
@@ -201,8 +196,7 @@ export type UnrecallableMetadataHandler<T> = {
|
||||
[UnrecallableMetadataKey]: true;
|
||||
type: string;
|
||||
parse: (metadata: unknown, store: AppStore) => Promise<T>;
|
||||
i18nKey: string;
|
||||
LabelComponent: ComponentType<{ i18nKey: string }>;
|
||||
LabelComponent: ComponentType;
|
||||
ValueComponent: ComponentType<UnrecallableMetadataValueProps<T>>;
|
||||
};
|
||||
|
||||
@@ -227,8 +221,7 @@ const CreatedBy: UnrecallableMetadataHandler<string> = {
|
||||
const parsed = z.string().parse(raw);
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
i18nKey: 'metadata.createdBy',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.createdBy" />,
|
||||
ValueComponent: ({ value }: UnrecallableMetadataValueProps<string>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Created By
|
||||
@@ -242,8 +235,7 @@ const GenerationMode: UnrecallableMetadataHandler<string> = {
|
||||
const parsed = z.string().parse(raw);
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
i18nKey: 'metadata.generationMode',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.generationMode" />,
|
||||
ValueComponent: ({ value }: UnrecallableMetadataValueProps<string>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Generation Mode
|
||||
@@ -260,8 +252,7 @@ const PositivePrompt: SingleMetadataHandler<ParameterPositivePrompt> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(positivePromptChanged(value));
|
||||
},
|
||||
i18nKey: 'metadata.positivePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.positivePrompt" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterPositivePrompt>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -280,8 +271,7 @@ const NegativePrompt: SingleMetadataHandler<ParameterNegativePrompt> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(negativePromptChanged(value || null));
|
||||
},
|
||||
i18nKey: 'metadata.negativePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.negativePrompt" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterNegativePrompt>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -300,8 +290,7 @@ const PositiveStylePrompt: SingleMetadataHandler<ParameterPositiveStylePromptSDX
|
||||
recall: (value, store) => {
|
||||
store.dispatch(positivePrompt2Changed(value));
|
||||
},
|
||||
i18nKey: 'sdxl.posStylePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.posStylePrompt" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterPositiveStylePromptSDXL>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -320,8 +309,7 @@ const NegativeStylePrompt: SingleMetadataHandler<ParameterPositiveStylePromptSDX
|
||||
recall: (value, store) => {
|
||||
store.dispatch(negativePrompt2Changed(value));
|
||||
},
|
||||
i18nKey: 'sdxl.negStylePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.negStylePrompt" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterPositiveStylePromptSDXL>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -340,8 +328,7 @@ const CFGScale: SingleMetadataHandler<ParameterCFGScale> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setCfgScale(value));
|
||||
},
|
||||
i18nKey: 'metadata.cfgScale',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.cfgScale" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterCFGScale>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion CFG Scale
|
||||
@@ -358,8 +345,7 @@ const CFGRescaleMultiplier: SingleMetadataHandler<ParameterCFGRescaleMultiplier>
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setCfgRescaleMultiplier(value));
|
||||
},
|
||||
i18nKey: 'metadata.cfgRescaleMultiplier',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.cfgRescaleMultiplier" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterCFGRescaleMultiplier>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -378,8 +364,7 @@ const Guidance: SingleMetadataHandler<ParameterGuidance> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setGuidance(value));
|
||||
},
|
||||
i18nKey: 'metadata.guidance',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.guidance" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterGuidance>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Guidance
|
||||
@@ -396,8 +381,7 @@ const Scheduler: SingleMetadataHandler<ParameterScheduler> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setScheduler(value));
|
||||
},
|
||||
i18nKey: 'metadata.scheduler',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.scheduler" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterScheduler>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Scheduler
|
||||
@@ -412,15 +396,9 @@ const Width: SingleMetadataHandler<ParameterWidth> = {
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
recall: (value, store) => {
|
||||
const activeTab = selectActiveTab(store.getState());
|
||||
if (activeTab === 'canvas') {
|
||||
store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true }));
|
||||
} else if (activeTab === 'generate') {
|
||||
store.dispatch(widthChanged({ width: value, updateAspectRatio: true, clamp: true }));
|
||||
}
|
||||
store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true }));
|
||||
},
|
||||
i18nKey: 'metadata.width',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.width" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterWidth>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Width
|
||||
@@ -435,15 +413,9 @@ const Height: SingleMetadataHandler<ParameterHeight> = {
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
recall: (value, store) => {
|
||||
const activeTab = selectActiveTab(store.getState());
|
||||
if (activeTab === 'canvas') {
|
||||
store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true }));
|
||||
} else if (activeTab === 'generate') {
|
||||
store.dispatch(heightChanged({ height: value, updateAspectRatio: true, clamp: true }));
|
||||
}
|
||||
store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true }));
|
||||
},
|
||||
i18nKey: 'metadata.height',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.height" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterHeight>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Height
|
||||
@@ -460,8 +432,7 @@ const Seed: SingleMetadataHandler<ParameterSeed> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setSeed(value));
|
||||
},
|
||||
i18nKey: 'metadata.seed',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.seed" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSeed>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Seed
|
||||
@@ -478,8 +449,7 @@ const Steps: SingleMetadataHandler<ParameterSteps> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setSteps(value));
|
||||
},
|
||||
i18nKey: 'metadata.steps',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.steps" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSteps>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion Steps
|
||||
@@ -496,8 +466,7 @@ const DenoisingStrength: SingleMetadataHandler<ParameterStrength> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setImg2imgStrength(value));
|
||||
},
|
||||
i18nKey: 'metadata.strength',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.strength" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterStrength>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion DenoisingStrength
|
||||
@@ -514,8 +483,7 @@ const SeamlessX: SingleMetadataHandler<ParameterSeamlessX> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setSeamlessXAxis(value));
|
||||
},
|
||||
i18nKey: 'metadata.seamlessXAxis',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.seamlessXAxis" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSeamlessX>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion SeamlessX
|
||||
@@ -532,8 +500,7 @@ const SeamlessY: SingleMetadataHandler<ParameterSeamlessY> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setSeamlessYAxis(value));
|
||||
},
|
||||
i18nKey: 'metadata.seamlessYAxis',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.seamlessYAxis" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSeamlessY>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion SeamlessY
|
||||
@@ -553,8 +520,7 @@ const RefinerModel: SingleMetadataHandler<ParameterSDXLRefinerModel> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(refinerModelChanged(value));
|
||||
},
|
||||
i18nKey: 'sdxl.refinermodel',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.refinermodel" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSDXLRefinerModel>) => (
|
||||
<MetadataPrimitiveValue value={`${value.name} (${value.base.toUpperCase()})`} />
|
||||
),
|
||||
@@ -573,8 +539,7 @@ const RefinerSteps: SingleMetadataHandler<ParameterSteps> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerSteps(value));
|
||||
},
|
||||
i18nKey: 'sdxl.refinerSteps',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.refinerSteps" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSteps>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion RefinerSteps
|
||||
@@ -591,8 +556,7 @@ const RefinerCFGScale: SingleMetadataHandler<ParameterCFGScale> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerCFGScale(value));
|
||||
},
|
||||
i18nKey: 'sdxl.cfgScale',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.cfgScale" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterCFGScale>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion RefinerCFGScale
|
||||
@@ -609,8 +573,7 @@ const RefinerScheduler: SingleMetadataHandler<ParameterScheduler> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerScheduler(value));
|
||||
},
|
||||
i18nKey: 'sdxl.scheduler',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.scheduler" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterScheduler>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion RefinerScheduler
|
||||
@@ -627,8 +590,7 @@ const RefinerPositiveAestheticScore: SingleMetadataHandler<ParameterSDXLRefinerP
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerPositiveAestheticScore(value));
|
||||
},
|
||||
i18nKey: 'sdxl.posAestheticScore',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.posAestheticScore" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSDXLRefinerPositiveAestheticScore>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -647,8 +609,7 @@ const RefinerNegativeAestheticScore: SingleMetadataHandler<ParameterSDXLRefinerN
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerNegativeAestheticScore(value));
|
||||
},
|
||||
i18nKey: 'sdxl.negAestheticScore',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.negAestheticScore" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSDXLRefinerNegativeAestheticScore>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -667,8 +628,7 @@ const RefinerDenoisingStart: SingleMetadataHandler<ParameterSDXLRefinerStart> =
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setRefinerStart(value));
|
||||
},
|
||||
i18nKey: 'sdxl.refinerStart',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="sdxl.refinerStart" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterSDXLRefinerStart>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
@@ -688,8 +648,7 @@ const MainModel: SingleMetadataHandler<ParameterModel> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(modelSelected(value));
|
||||
},
|
||||
i18nKey: 'metadata.model',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.model" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterModel>) => (
|
||||
<MetadataPrimitiveValue value={`${value.name} (${value.base.toUpperCase()})`} />
|
||||
),
|
||||
@@ -710,8 +669,7 @@ const VAEModel: SingleMetadataHandler<ParameterVAEModel> = {
|
||||
recall: (value, store) => {
|
||||
store.dispatch(vaeSelected(value));
|
||||
},
|
||||
i18nKey: 'metadata.vae',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.vae" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterVAEModel>) => (
|
||||
<MetadataPrimitiveValue value={`${value.name} (${value.base.toUpperCase()})`} />
|
||||
),
|
||||
@@ -775,8 +733,7 @@ const LoRAs: CollectionMetadataHandler<LoRA[]> = {
|
||||
store.dispatch(loraRecalled({ lora }));
|
||||
}
|
||||
},
|
||||
i18nKey: 'models.lora',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="models.lora" />,
|
||||
ValueComponent: ({ value }: CollectionMetadataValueProps<LoRA[]>) => (
|
||||
<MetadataPrimitiveValue value={`${value.model.name} (${value.model.base.toUpperCase()}) - ${value.weight}`} />
|
||||
),
|
||||
@@ -806,8 +763,7 @@ const CanvasLayers: SingleMetadataHandler<CanvasMetadata> = {
|
||||
}
|
||||
store.dispatch(canvasMetadataRecalled(value));
|
||||
},
|
||||
i18nKey: 'metadata.canvasV2Metadata',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="metadata.canvasV2Metadata" />,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<CanvasMetadata>) => {
|
||||
const { t } = useTranslation();
|
||||
const count =
|
||||
@@ -854,8 +810,7 @@ const RefImages: CollectionMetadataHandler<RefImageState[]> = {
|
||||
const entities = [{ ...data, id: getPrefixedId('reference_image') }];
|
||||
store.dispatch(refImagesRecalled({ entities, replace: false }));
|
||||
},
|
||||
i18nKey: 'controlLayers.referenceImage',
|
||||
LabelComponent: MetadataLabel,
|
||||
LabelComponent: () => <MetadataLabel i18nKey="controlLayers.referenceImage" />,
|
||||
ValueComponent: ({ value }: CollectionMetadataValueProps<RefImageState[]>) => {
|
||||
if (value.config.model) {
|
||||
return <MetadataPrimitiveValue value={value.config.model.name} />;
|
||||
@@ -907,7 +862,7 @@ export const MetadataHandlers = {
|
||||
// ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer,
|
||||
} as const;
|
||||
|
||||
const successToast = (parameter: string) => {
|
||||
const successToast = (parameter: ReactNode) => {
|
||||
toast({
|
||||
id: 'PARAMETER_SET',
|
||||
title: t('toast.parameterSet'),
|
||||
@@ -916,7 +871,7 @@ const successToast = (parameter: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const failedToast = (parameter: string, message?: string) => {
|
||||
const failedToast = (parameter: ReactNode, message?: ReactNode) => {
|
||||
toast({
|
||||
id: 'PARAMETER_NOT_SET',
|
||||
title: t('toast.parameterNotSet'),
|
||||
@@ -947,9 +902,9 @@ const recallByHandler = async (arg: {
|
||||
|
||||
if (!silent) {
|
||||
if (didRecall) {
|
||||
successToast(t(handler.i18nKey));
|
||||
successToast(<handler.LabelComponent />);
|
||||
} else {
|
||||
failedToast(t(handler.i18nKey));
|
||||
failedToast(<handler.LabelComponent />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,28 +1006,6 @@ const recallPrompts = async (metadata: unknown, store: AppStore) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasMetadataByHandlers = async (arg: {
|
||||
metadata: unknown;
|
||||
handlers: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[];
|
||||
store: AppStore;
|
||||
require: 'some' | 'all';
|
||||
}) => {
|
||||
const { metadata, handlers, store, require } = arg;
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler.parse(metadata, store);
|
||||
if (require === 'some') {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
if (require === 'all') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const recallDimensions = async (metadata: unknown, store: AppStore) => {
|
||||
const recalled = await recallByHandlers({
|
||||
metadata,
|
||||
@@ -1102,7 +1035,6 @@ const recallAll = async (
|
||||
};
|
||||
|
||||
export const MetadataUtils = {
|
||||
hasMetadataByHandlers,
|
||||
recallByHandler,
|
||||
recallByHandlers,
|
||||
recallAll,
|
||||
|
||||
@@ -32,7 +32,7 @@ const CurrentImageNode = (props: NodeProps) => {
|
||||
if (imageDTO) {
|
||||
return (
|
||||
<Wrapper nodeProps={props}>
|
||||
<DndImage imageDTO={imageDTO} borderRadius="base" />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,6 @@ const ImageGridItemContent = memo(
|
||||
return (
|
||||
<>
|
||||
<DndImage
|
||||
borderRadius="base"
|
||||
imageDTO={query.data}
|
||||
asThumbnail
|
||||
objectFit="contain"
|
||||
|
||||
@@ -76,7 +76,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
)}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<Flex borderRadius="base" borderWidth={1} borderStyle="solid" overflow="hidden">
|
||||
<Flex borderRadius="base" borderWidth={1} borderStyle="solid">
|
||||
<DndImage imageDTO={imageDTO} asThumbnail />
|
||||
</Flex>
|
||||
<Text
|
||||
|
||||
@@ -14,7 +14,7 @@ const ImageOutputPreview = ({ output }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DndImage imageDTO={imageDTO} borderRadius="base" />;
|
||||
return <DndImage imageDTO={imageDTO} />;
|
||||
};
|
||||
|
||||
export default memo(ImageOutputPreview);
|
||||
|
||||
@@ -30,14 +30,11 @@ export const addFLUXFill = async ({
|
||||
denoise.denoising_start = denoising_start;
|
||||
denoise.denoising_end = denoising_end;
|
||||
|
||||
const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state);
|
||||
|
||||
denoise.width = scaledSize.width;
|
||||
denoise.height = scaledSize.height;
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
const canvasSettings = selectCanvasSettingsSlice(state);
|
||||
|
||||
const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state);
|
||||
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
|
||||
is_intermediate: true,
|
||||
|
||||
@@ -78,6 +78,8 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise<GraphBuilder
|
||||
if (generationMode !== 'txt2img') {
|
||||
throw new UnsupportedGenerationModeError(t('toast.fluxKontextIncompatibleGenerationMode'));
|
||||
}
|
||||
|
||||
guidance = 30;
|
||||
}
|
||||
|
||||
const g = new Graph(getPrefixedId('flux_graph'));
|
||||
|
||||
@@ -26,7 +26,7 @@ export const buildFluxKontextGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
assert(model.base === 'flux-kontext', 'Selected model is not a FLUX Kontext API model');
|
||||
|
||||
if (generationMode !== 'txt2img') {
|
||||
throw new UnsupportedGenerationModeError(t('toast.fluxKontextIncompatibleGenerationMode'));
|
||||
throw new UnsupportedGenerationModeError(t('toast.imagenIncompatibleGenerationMode', { model: 'FLUX Kontext' }));
|
||||
}
|
||||
|
||||
log.debug({ generationMode, manager: manager?.id }, 'Building FLUX Kontext graph');
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { Group, PickerContextState } from 'common/components/Picker/Picker';
|
||||
import { buildGroup, getRegex, isGroup, Picker, usePickerContext } from 'common/components/Picker/Picker';
|
||||
import { buildGroup, getRegex, isOption, Picker, usePickerContext } from 'common/components/Picker/Picker';
|
||||
import { useDisclosure } from 'common/hooks/useBoolean';
|
||||
import { typedMemo } from 'common/util/typedMemo';
|
||||
import { uniq } from 'es-toolkit/compat';
|
||||
@@ -277,22 +277,8 @@ export const ModelPicker = typedMemo(
|
||||
if (!selectedModelConfig) {
|
||||
return undefined;
|
||||
}
|
||||
let _selectedOption: WithStarred<T> | undefined = undefined;
|
||||
|
||||
for (const optionOrGroup of options) {
|
||||
if (isGroup(optionOrGroup)) {
|
||||
const result = optionOrGroup.options.find((o) => o.key === selectedModelConfig.key);
|
||||
if (result) {
|
||||
_selectedOption = result;
|
||||
break;
|
||||
}
|
||||
} else if (optionOrGroup.key === selectedModelConfig.key) {
|
||||
_selectedOption = optionOrGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _selectedOption;
|
||||
return options.filter(isOption).find((o) => o.key === selectedModelConfig.key);
|
||||
}, [options, selectedModelConfig]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
@@ -375,19 +361,9 @@ const optionSx: SystemStyleObject = {
|
||||
cursor: 'pointer',
|
||||
borderRadius: 'base',
|
||||
'&[data-selected="true"]': {
|
||||
bg: 'invokeBlue.300',
|
||||
color: 'base.900',
|
||||
'.extra-info': {
|
||||
color: 'base.700',
|
||||
},
|
||||
'.picker-option': {
|
||||
fontWeight: 'bold',
|
||||
'&[data-is-compact="true"]': {
|
||||
fontWeight: 'semibold',
|
||||
},
|
||||
},
|
||||
bg: 'base.700',
|
||||
'&[data-active="true"]': {
|
||||
bg: 'invokeBlue.250',
|
||||
bg: 'base.650',
|
||||
},
|
||||
},
|
||||
'&[data-active="true"]': {
|
||||
@@ -424,31 +400,17 @@ const PickerOptionComponent = typedMemo(
|
||||
<Flex flexDir="column" gap={1} flex={1}>
|
||||
<Flex gap={2} alignItems="center">
|
||||
{option.starred && <Icon as={PiLinkSimple} color="invokeYellow.500" boxSize={4} />}
|
||||
<Text className="picker-option" sx={optionNameSx} data-is-compact={compactView}>
|
||||
<Text sx={optionNameSx} data-is-compact={compactView}>
|
||||
{option.name}
|
||||
</Text>
|
||||
<Spacer />
|
||||
{option.file_size > 0 && (
|
||||
<Text
|
||||
className="extra-info"
|
||||
variant="subtext"
|
||||
fontStyle="italic"
|
||||
noOfLines={1}
|
||||
flexShrink={0}
|
||||
overflow="visible"
|
||||
>
|
||||
<Text variant="subtext" fontStyle="italic" noOfLines={1} flexShrink={0} overflow="visible">
|
||||
{filesize(option.file_size)}
|
||||
</Text>
|
||||
)}
|
||||
{option.usage_info && (
|
||||
<Text
|
||||
className="extra-info"
|
||||
variant="subtext"
|
||||
fontStyle="italic"
|
||||
noOfLines={1}
|
||||
flexShrink={0}
|
||||
overflow="visible"
|
||||
>
|
||||
<Text variant="subtext" fontStyle="italic" noOfLines={1} flexShrink={0} overflow="visible">
|
||||
{option.usage_info}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -287,19 +287,17 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.fluxKontextMultipleReferenceImages') });
|
||||
}
|
||||
|
||||
refImages.entities
|
||||
.filter(({ isEnabled }) => isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
refImages.entities.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const UpscaleInitialImage = () => {
|
||||
{!imageDTO && <UploadImageIconButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} borderRadius="base" />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={onReset}
|
||||
|
||||
@@ -144,6 +144,7 @@ export const useHotkeyData = (): HotkeysData => {
|
||||
addHotkey('viewer', 'recallPrompts', ['p']);
|
||||
addHotkey('viewer', 'remix', ['r']);
|
||||
addHotkey('viewer', 'useSize', ['d']);
|
||||
addHotkey('viewer', 'runPostprocessing', ['shift+u']);
|
||||
addHotkey('viewer', 'toggleMetadata', ['i']);
|
||||
|
||||
// Gallery
|
||||
|
||||
@@ -58,7 +58,7 @@ const TabContent = memo(() => {
|
||||
TabContent.displayName = 'TabContent';
|
||||
|
||||
const SwitchingTabsLoader = memo(() => {
|
||||
const isSwitchingTabs = useStore(navigationApi.$isLoading);
|
||||
const isSwitchingTabs = useStore(navigationApi.$isSwitchingTabs);
|
||||
|
||||
if (isSwitchingTabs) {
|
||||
return <Loading />;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
||||
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
@@ -21,8 +23,6 @@ import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagin
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
|
||||
import { StagingArea } from './StagingArea';
|
||||
|
||||
const MenuContent = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
@@ -106,7 +106,23 @@ export const CanvasWorkspacePanel = memo(() => {
|
||||
{canvasId !== null && (
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider type="advanced" id={canvasId}>
|
||||
<StagingArea />
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
bottom={4}
|
||||
gap={2}
|
||||
align="center"
|
||||
justify="center"
|
||||
left={4}
|
||||
right={4}
|
||||
>
|
||||
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||
<StagingAreaItemsList />
|
||||
</Flex>
|
||||
<Flex gap={2}>
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StagingArea = memo(() => {
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
if (!isStaging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex position="absolute" flexDir="column" bottom={4} gap={2} align="center" justify="center" left={4} right={4}>
|
||||
<StagingAreaItemsList />
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
StagingArea.displayName = 'StagingArea';
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
|
||||
import type {
|
||||
DockviewApi,
|
||||
GridviewApi,
|
||||
IDockviewPanel,
|
||||
IDockviewReactProps,
|
||||
IGridviewPanel,
|
||||
IGridviewReactProps,
|
||||
} from 'dockview';
|
||||
import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel';
|
||||
@@ -64,50 +71,54 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
};
|
||||
|
||||
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Canvas',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'canvas',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Canvas',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'canvas',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, WORKSPACE_PANEL_ID, workspace);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const MainPanel = memo(() => {
|
||||
@@ -146,49 +157,54 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: LAYERS_PANEL_ID,
|
||||
component: LAYERS_PANEL_ID,
|
||||
minimumHeight: LAYERS_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'layers',
|
||||
},
|
||||
position: {
|
||||
direction: 'below',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX });
|
||||
boards.api.setSize({ height: CANVAS_BOARD_PANEL_DEFAULT_HEIGHT_PX });
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const layers = api.addPanel<PanelParameters>({
|
||||
id: LAYERS_PANEL_ID,
|
||||
component: LAYERS_PANEL_ID,
|
||||
minimumHeight: LAYERS_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'layers',
|
||||
},
|
||||
position: {
|
||||
direction: 'below',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: CANVAS_BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, LAYERS_PANEL_ID, layers);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, layers, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
@@ -216,16 +232,19 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
@@ -254,44 +273,47 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
priority: LayoutPriority.Low,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
priority: LayoutPriority.Low,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('canvas', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('canvas', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('canvas', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const CanvasTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('canvas', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('canvas');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
|
||||
import type {
|
||||
DockviewApi,
|
||||
GridviewApi,
|
||||
IDockviewPanel,
|
||||
IDockviewReactProps,
|
||||
IGridviewPanel,
|
||||
IGridviewReactProps,
|
||||
} from 'dockview';
|
||||
import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
|
||||
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
|
||||
@@ -58,35 +65,38 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
};
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const MainPanel = memo(() => {
|
||||
@@ -124,35 +134,39 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX });
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
@@ -180,16 +194,19 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
@@ -218,42 +235,47 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
const main = api.addPanel<PanelParameters>({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel<PanelParameters>({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel<PanelParameters>({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const main = layoutApi.addPanel<PanelParameters>({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = layoutApi.addPanel<PanelParameters>({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = layoutApi.addPanel<PanelParameters>({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('generate', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const GenerateTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('generate', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('generate');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { GridviewApi, IGridviewReactProps } from 'dockview';
|
||||
import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview';
|
||||
import { GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||
import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import { navigationApi } from './navigation-api';
|
||||
@@ -13,19 +12,22 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[MODELS_PANEL_ID]: ModelManagerTab,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
api.addPanel({
|
||||
id: MODELS_PANEL_ID,
|
||||
component: MODELS_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const models = layoutApi.addPanel({
|
||||
id: MODELS_PANEL_ID,
|
||||
component: MODELS_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
navigationApi.registerPanel('models', MODELS_PANEL_ID, models);
|
||||
|
||||
return { models } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const ModelsTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('models', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('models');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DockviewApi, GridviewApi } from 'dockview';
|
||||
import { DockviewPanel, GridviewPanel } from 'dockview';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
RIGHT_PANEL_ID,
|
||||
RIGHT_PANEL_MIN_SIZE_PX,
|
||||
SETTINGS_PANEL_ID,
|
||||
SWITCH_TABS_FAKE_DELAY_MS,
|
||||
WORKSPACE_PANEL_ID,
|
||||
} from './shared';
|
||||
|
||||
@@ -22,7 +20,6 @@ vi.mock('app/logging/logger', () => ({
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -31,9 +28,8 @@ vi.mock('dockview', async () => {
|
||||
|
||||
// Mock GridviewPanel class for instanceof checks
|
||||
class MockGridviewPanel {
|
||||
maximumWidth?: number;
|
||||
minimumWidth?: number;
|
||||
width?: number;
|
||||
maximumWidth: number;
|
||||
minimumWidth: number;
|
||||
api = {
|
||||
setActive: vi.fn(),
|
||||
setConstraints: vi.fn(),
|
||||
@@ -41,10 +37,9 @@ vi.mock('dockview', async () => {
|
||||
onDidDimensionsChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
};
|
||||
|
||||
constructor(config: { maximumWidth?: number; minimumWidth?: number; width?: number } = {}) {
|
||||
this.maximumWidth = config.maximumWidth;
|
||||
this.minimumWidth = config.minimumWidth;
|
||||
this.width = config.width;
|
||||
constructor(config: { maximumWidth?: number; minimumWidth?: number } = {}) {
|
||||
this.maximumWidth = config.maximumWidth ?? Number.MAX_SAFE_INTEGER;
|
||||
this.minimumWidth = config.minimumWidth ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +61,7 @@ vi.mock('dockview', async () => {
|
||||
});
|
||||
|
||||
// Mock panel with setActive method
|
||||
const createMockPanel = (config: { maximumWidth?: number; minimumWidth?: number; width?: number } = {}) => {
|
||||
const createMockPanel = (config: { maximumWidth?: number; minimumWidth?: number } = {}) => {
|
||||
/* @ts-expect-error we are mocking GridviewPanel to be a concrete class */
|
||||
return new GridviewPanel(config);
|
||||
};
|
||||
@@ -80,27 +75,27 @@ describe('AppNavigationApi', () => {
|
||||
let navigationApi: NavigationApi;
|
||||
let mockSetAppTab: ReturnType<typeof vi.fn>;
|
||||
let mockGetAppTab: ReturnType<typeof vi.fn>;
|
||||
let mockSetStorage: ReturnType<typeof vi.fn>;
|
||||
let mockGetStorage: ReturnType<typeof vi.fn>;
|
||||
let mockDeleteStorage: ReturnType<typeof vi.fn>;
|
||||
let mockSetPanelState: ReturnType<typeof vi.fn>;
|
||||
let mockGetPanelState: ReturnType<typeof vi.fn>;
|
||||
let mockDeletePanelState: ReturnType<typeof vi.fn>;
|
||||
let mockAppApi: NavigationAppApi;
|
||||
|
||||
beforeEach(() => {
|
||||
navigationApi = new NavigationApi();
|
||||
mockSetAppTab = vi.fn();
|
||||
mockGetAppTab = vi.fn();
|
||||
mockSetStorage = vi.fn();
|
||||
mockGetStorage = vi.fn();
|
||||
mockDeleteStorage = vi.fn();
|
||||
mockSetPanelState = vi.fn();
|
||||
mockGetPanelState = vi.fn();
|
||||
mockDeletePanelState = vi.fn();
|
||||
mockAppApi = {
|
||||
activeTab: {
|
||||
set: mockSetAppTab,
|
||||
get: mockGetAppTab,
|
||||
},
|
||||
storage: {
|
||||
set: mockSetStorage,
|
||||
get: mockGetStorage,
|
||||
delete: mockDeleteStorage,
|
||||
panelStorage: {
|
||||
set: mockSetPanelState,
|
||||
get: mockGetPanelState,
|
||||
delete: mockDeletePanelState,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -120,9 +115,9 @@ describe('AppNavigationApi', () => {
|
||||
expect(navigationApi._app).not.toBeNull();
|
||||
expect(navigationApi._app?.activeTab.set).toBe(mockSetAppTab);
|
||||
expect(navigationApi._app?.activeTab.get).toBe(mockGetAppTab);
|
||||
expect(navigationApi._app?.storage.set).toBe(mockSetStorage);
|
||||
expect(navigationApi._app?.storage.get).toBe(mockGetStorage);
|
||||
expect(navigationApi._app?.storage.delete).toBe(mockDeleteStorage);
|
||||
expect(navigationApi._app?.panelStorage.set).toBe(mockSetPanelState);
|
||||
expect(navigationApi._app?.panelStorage.get).toBe(mockGetPanelState);
|
||||
expect(navigationApi._app?.panelStorage.delete).toBe(mockDeletePanelState);
|
||||
});
|
||||
|
||||
it('should disconnect from app', () => {
|
||||
@@ -133,89 +128,10 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Switching', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should switch tabs', () => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('canvas');
|
||||
});
|
||||
|
||||
it('should not set the tab if it is already on that tab', () => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
mockGetAppTab.mockReturnValue('canvas');
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(mockSetAppTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set the $isLoading atom when switching', () => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
});
|
||||
|
||||
it('should unset the $isLoading atom after a fake delay', () => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(SWITCH_TABS_FAKE_DELAY_MS);
|
||||
expect(navigationApi.$isLoading.get()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle rapid tab changes', () => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
navigationApi.switchToTab('generate');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(SWITCH_TABS_FAKE_DELAY_MS / 5);
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(SWITCH_TABS_FAKE_DELAY_MS / 5);
|
||||
|
||||
navigationApi.switchToTab('generate');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(SWITCH_TABS_FAKE_DELAY_MS / 5);
|
||||
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(navigationApi.$isLoading.get()).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(SWITCH_TABS_FAKE_DELAY_MS);
|
||||
|
||||
expect(navigationApi.$isLoading.get()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not switch tabs if the app is not connected', () => {
|
||||
navigationApi.switchToTab('canvas');
|
||||
expect(mockSetAppTab).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Registration', () => {
|
||||
it('should register and unregister panels', () => {
|
||||
const mockPanel = createMockPanel();
|
||||
const unregister = navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
expect(typeof unregister).toBe('function');
|
||||
expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
|
||||
@@ -232,7 +148,7 @@ describe('AppNavigationApi', () => {
|
||||
const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
|
||||
|
||||
// Register the panel
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
// Wait should resolve
|
||||
await expect(waitPromise).resolves.toBeUndefined();
|
||||
@@ -242,8 +158,8 @@ describe('AppNavigationApi', () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockDockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
|
||||
expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
|
||||
expect(navigationApi.isPanelRegistered('generate', LAUNCHPAD_PANEL_ID)).toBe(true);
|
||||
@@ -257,8 +173,8 @@ describe('AppNavigationApi', () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi._registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel2);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel2);
|
||||
|
||||
expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
|
||||
expect(navigationApi.isPanelRegistered('canvas', SETTINGS_PANEL_ID)).toBe(true);
|
||||
@@ -269,6 +185,95 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Storage', () => {
|
||||
beforeEach(() => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
});
|
||||
|
||||
it('stores initial gridview state when none exists', () => {
|
||||
const key = `generate:${LEFT_PANEL_ID}`;
|
||||
mockGetPanelState.mockReturnValue(undefined);
|
||||
|
||||
const panel = createMockPanel();
|
||||
// simulate real dimensions
|
||||
panel.api.height = 200;
|
||||
panel.api.width = 400;
|
||||
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, panel);
|
||||
|
||||
expect(mockGetPanelState).toHaveBeenCalledWith(key);
|
||||
expect(mockSetPanelState).toHaveBeenCalledWith(key, {
|
||||
id: key,
|
||||
type: 'gridview-panel',
|
||||
dimensions: { height: 200, width: 400 },
|
||||
});
|
||||
});
|
||||
|
||||
it('restores gridview from stored state', () => {
|
||||
const key = `generate:${LEFT_PANEL_ID}`;
|
||||
const stored = { id: key, type: 'gridview-panel', dimensions: { height: 50, width: 75 } };
|
||||
mockGetPanelState.mockReturnValue(stored);
|
||||
|
||||
const panel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, panel);
|
||||
|
||||
expect(panel.api.setSize).toHaveBeenCalledWith({ height: 50, width: 75 });
|
||||
expect(mockDeletePanelState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('collapses gridview when stored dimensions are zero', () => {
|
||||
const key = `generate:${LEFT_PANEL_ID}`;
|
||||
const stored = { id: key, type: 'gridview-panel', dimensions: { height: 0, width: 0 } };
|
||||
mockGetPanelState.mockReturnValue(stored);
|
||||
|
||||
const panel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, panel);
|
||||
|
||||
expect(panel.api.setConstraints).toHaveBeenCalledWith({ minimumWidth: 0, maximumWidth: 0 });
|
||||
expect(panel.api.setConstraints).toHaveBeenCalledWith({ minimumHeight: 0, maximumHeight: 0 });
|
||||
expect(panel.api.setSize).toHaveBeenCalledWith({ height: 0, width: 0 });
|
||||
});
|
||||
|
||||
it('stores initial dockview state when none exists', () => {
|
||||
const key = `generate:${LAUNCHPAD_PANEL_ID}`;
|
||||
mockGetPanelState.mockReturnValue(undefined);
|
||||
|
||||
const panel = createMockDockPanel();
|
||||
Object.defineProperty(panel.api, 'isActive', { value: true });
|
||||
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, panel);
|
||||
|
||||
expect(mockGetPanelState).toHaveBeenCalledWith(key);
|
||||
expect(mockSetPanelState).toHaveBeenCalledWith(key, {
|
||||
id: key,
|
||||
type: 'dockview-panel',
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('restores dockview active state', () => {
|
||||
const key = `generate:${LAUNCHPAD_PANEL_ID}`;
|
||||
const stored = { id: key, type: 'dockview-panel', isActive: true };
|
||||
mockGetPanelState.mockReturnValue(stored);
|
||||
|
||||
const panel = createMockDockPanel();
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, panel);
|
||||
|
||||
expect(panel.api.setActive).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes mismatched dockview state', () => {
|
||||
const key = `generate:${LAUNCHPAD_PANEL_ID}`;
|
||||
const stored = { id: key, type: 'gridview-panel', dimensions: { height: 5, width: 5 } };
|
||||
mockGetPanelState.mockReturnValue(stored);
|
||||
|
||||
const panel = createMockDockPanel();
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, panel);
|
||||
|
||||
expect(mockDeletePanelState).toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Focus', () => {
|
||||
beforeEach(() => {
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
@@ -276,7 +281,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should focus panel in already registered tab', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -288,7 +293,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should switch tab before focusing panel', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('canvas'); // Currently on different tab
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -307,7 +312,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
// Register panel after a short delay
|
||||
setTimeout(() => {
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
}, 100);
|
||||
|
||||
const result = await focusPromise;
|
||||
@@ -320,8 +325,8 @@ describe('AppNavigationApi', () => {
|
||||
const mockGridPanel = createMockPanel();
|
||||
const mockDockPanel = createMockDockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockGridPanel);
|
||||
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockDockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockGridPanel);
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockDockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Test gridview panel
|
||||
@@ -352,7 +357,7 @@ describe('AppNavigationApi', () => {
|
||||
throw new Error('Mock error');
|
||||
});
|
||||
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -362,7 +367,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should work without app connection', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
// Don't connect to app
|
||||
const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -375,7 +380,7 @@ describe('AppNavigationApi', () => {
|
||||
describe('Panel Waiting', () => {
|
||||
it('should resolve immediately for already registered panels', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
|
||||
|
||||
@@ -389,7 +394,7 @@ describe('AppNavigationApi', () => {
|
||||
const waitPromise2 = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
|
||||
|
||||
setTimeout(() => {
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
}, 50);
|
||||
|
||||
await expect(Promise.all([waitPromise1, waitPromise2])).resolves.toEqual([undefined, undefined]);
|
||||
@@ -398,14 +403,14 @@ describe('AppNavigationApi', () => {
|
||||
it('should timeout if panel is not registered', async () => {
|
||||
const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 100);
|
||||
|
||||
await expect(waitPromise).rejects.toThrow(/Panel .* registration timed out after 100ms/);
|
||||
await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 100ms');
|
||||
});
|
||||
|
||||
it('should handle custom timeout', async () => {
|
||||
const start = Date.now();
|
||||
const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 200);
|
||||
|
||||
await expect(waitPromise).rejects.toThrow(/Panel .* registration timed out after 200ms/);
|
||||
await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 200ms');
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
// TODO(psyche): Use vitest's fake timeres
|
||||
@@ -421,9 +426,9 @@ describe('AppNavigationApi', () => {
|
||||
const mockPanel2 = createMockPanel();
|
||||
const mockPanel3 = createMockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
navigationApi._registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel3);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel3);
|
||||
|
||||
expect(navigationApi.getRegisteredPanels('generate')).toHaveLength(2);
|
||||
expect(navigationApi.getRegisteredPanels('canvas')).toHaveLength(1);
|
||||
@@ -453,7 +458,7 @@ describe('AppNavigationApi', () => {
|
||||
mockGetAppTab.mockReturnValue('canvas');
|
||||
|
||||
// Register panel
|
||||
const unregister = navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
// Focus panel (should switch tab and focus)
|
||||
const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -479,9 +484,9 @@ describe('AppNavigationApi', () => {
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Register panels
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
navigationApi._registerPanel('canvas', WORKSPACE_PANEL_ID, mockPanel3);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
|
||||
navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
|
||||
navigationApi.registerPanel('canvas', WORKSPACE_PANEL_ID, mockPanel3);
|
||||
|
||||
// Focus panels
|
||||
await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
|
||||
@@ -505,7 +510,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
// Register after delay
|
||||
setTimeout(() => {
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
}, 50);
|
||||
|
||||
const result = await focusPromise;
|
||||
@@ -522,7 +527,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should focus panel in active tab', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID);
|
||||
@@ -578,7 +583,7 @@ describe('AppNavigationApi', () => {
|
||||
describe('getPanel', () => {
|
||||
it('should return registered panel', () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
|
||||
|
||||
const result = navigationApi.getPanel('generate', SETTINGS_PANEL_ID);
|
||||
|
||||
@@ -604,8 +609,8 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
|
||||
it('should expand collapsed left panel', () => {
|
||||
const mockPanel = createMockPanel({ width: 0 });
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
const mockPanel = createMockPanel({ maximumWidth: 0 });
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftPanel();
|
||||
@@ -620,7 +625,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should collapse expanded left panel', () => {
|
||||
const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftPanel();
|
||||
@@ -651,7 +656,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should return false when panel is not GridviewPanel', () => {
|
||||
const mockPanel = createMockDockPanel();
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftPanel();
|
||||
@@ -666,8 +671,8 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
|
||||
it('should expand collapsed right panel', () => {
|
||||
const mockPanel = createMockPanel({ width: 0 });
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
const mockPanel = createMockPanel({ maximumWidth: 0 });
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleRightPanel();
|
||||
@@ -682,7 +687,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should collapse expanded right panel', () => {
|
||||
const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleRightPanel();
|
||||
@@ -713,7 +718,7 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should return false when panel is not GridviewPanel', () => {
|
||||
const mockPanel = createMockDockPanel();
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleRightPanel();
|
||||
@@ -728,11 +733,11 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
|
||||
it('should expand both panels when left is collapsed', () => {
|
||||
const leftPanel = createMockPanel({ width: 0 });
|
||||
const leftPanel = createMockPanel({ maximumWidth: 0 });
|
||||
const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftAndRightPanels();
|
||||
@@ -752,10 +757,10 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should expand both panels when right is collapsed', () => {
|
||||
const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
const rightPanel = createMockPanel({ width: 0 });
|
||||
const rightPanel = createMockPanel({ maximumWidth: 0 });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftAndRightPanels();
|
||||
@@ -777,8 +782,8 @@ describe('AppNavigationApi', () => {
|
||||
const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftAndRightPanels();
|
||||
@@ -797,11 +802,11 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
|
||||
it('should expand both panels when both are collapsed', () => {
|
||||
const leftPanel = createMockPanel({ width: 0 });
|
||||
const rightPanel = createMockPanel({ width: 0 });
|
||||
const leftPanel = createMockPanel({ maximumWidth: 0 });
|
||||
const rightPanel = createMockPanel({ maximumWidth: 0 });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftAndRightPanels();
|
||||
@@ -839,8 +844,8 @@ describe('AppNavigationApi', () => {
|
||||
const leftPanel = createMockDockPanel();
|
||||
const rightPanel = createMockDockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.toggleLeftAndRightPanels();
|
||||
@@ -855,11 +860,11 @@ describe('AppNavigationApi', () => {
|
||||
});
|
||||
|
||||
it('should reset both panels to expanded state', () => {
|
||||
const leftPanel = createMockPanel({ width: 0 });
|
||||
const rightPanel = createMockPanel({ width: 0 });
|
||||
const leftPanel = createMockPanel({ maximumWidth: 0 });
|
||||
const rightPanel = createMockPanel({ maximumWidth: 0 });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.resetLeftAndRightPanels();
|
||||
@@ -900,8 +905,8 @@ describe('AppNavigationApi', () => {
|
||||
const leftPanel = createMockDockPanel();
|
||||
const rightPanel = createMockDockPanel();
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = navigationApi.resetLeftAndRightPanels();
|
||||
@@ -921,9 +926,9 @@ describe('AppNavigationApi', () => {
|
||||
const settingsPanel = createMockPanel();
|
||||
|
||||
// Register panels
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi._registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, settingsPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
|
||||
navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, settingsPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Focus a panel in active tab
|
||||
@@ -950,10 +955,10 @@ describe('AppNavigationApi', () => {
|
||||
|
||||
it('should handle tab switching with panel operations', () => {
|
||||
const generateLeftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
|
||||
const canvasLeftPanel = createMockPanel({ width: 0 });
|
||||
const canvasLeftPanel = createMockPanel({ maximumWidth: 0 });
|
||||
|
||||
navigationApi._registerPanel('generate', LEFT_PANEL_ID, generateLeftPanel);
|
||||
navigationApi._registerPanel('canvas', LEFT_PANEL_ID, canvasLeftPanel);
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, generateLeftPanel);
|
||||
navigationApi.registerPanel('canvas', LEFT_PANEL_ID, canvasLeftPanel);
|
||||
|
||||
// Start on generate tab
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
@@ -987,122 +992,4 @@ describe('AppNavigationApi', () => {
|
||||
expect(focusResult).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerContainer', () => {
|
||||
const tab = 'generate';
|
||||
const viewId = 'myView';
|
||||
const key = `${tab}:container:${viewId}`;
|
||||
|
||||
beforeEach(() => {
|
||||
navigationApi = new NavigationApi();
|
||||
navigationApi.connectToApp(mockAppApi);
|
||||
});
|
||||
|
||||
it('initializes from scratch when no stored state', () => {
|
||||
mockGetStorage.mockReturnValue(undefined);
|
||||
const initialize = vi.fn();
|
||||
const panel1 = { id: 'p1' };
|
||||
const panel2 = { id: 'p2' };
|
||||
const mockApi = {
|
||||
panels: [panel1, panel2],
|
||||
toJSON: vi.fn(() => ({ foo: 'bar' })),
|
||||
onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
} as unknown as DockviewApi | GridviewApi;
|
||||
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
|
||||
|
||||
expect(initialize).toHaveBeenCalledOnce();
|
||||
expect(mockSetStorage).toHaveBeenCalledOnce();
|
||||
expect(mockSetStorage).toHaveBeenCalledWith(key, { foo: 'bar' });
|
||||
// panels registered
|
||||
expect(navigationApi.isPanelRegistered(tab, 'p1')).toBe(true);
|
||||
expect(navigationApi.isPanelRegistered(tab, 'p2')).toBe(true);
|
||||
});
|
||||
|
||||
it('restores from storage when fromJSON succeeds', () => {
|
||||
const stored = { saved: true };
|
||||
mockGetStorage.mockReturnValue(stored);
|
||||
const initialize = vi.fn();
|
||||
const panel = { id: 'p' };
|
||||
const mockApi = {
|
||||
panels: [panel],
|
||||
fromJSON: vi.fn(),
|
||||
toJSON: vi.fn(),
|
||||
onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
} as unknown as DockviewApi | GridviewApi;
|
||||
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
|
||||
|
||||
expect(mockApi.fromJSON).toHaveBeenCalledWith(stored);
|
||||
expect(initialize).not.toHaveBeenCalled();
|
||||
expect(mockSetStorage).not.toHaveBeenCalled(); // no initial persist
|
||||
expect(navigationApi.isPanelRegistered(tab, 'p')).toBe(true);
|
||||
});
|
||||
|
||||
it('re-initializes when fromJSON throws, deletes then sets', () => {
|
||||
const stored = { saved: true };
|
||||
mockGetStorage.mockReturnValue(stored);
|
||||
const initialize = vi.fn();
|
||||
const panel = { id: 'p' };
|
||||
const mockApi = {
|
||||
panels: [panel],
|
||||
fromJSON: vi.fn(() => {
|
||||
throw new Error('bad');
|
||||
}),
|
||||
toJSON: vi.fn(() => ({ new: 'state' })),
|
||||
onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
} as unknown as DockviewApi | GridviewApi;
|
||||
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
|
||||
|
||||
expect(mockApi.fromJSON).toHaveBeenCalledWith(stored);
|
||||
expect(mockDeleteStorage).toHaveBeenCalledOnce();
|
||||
expect(mockDeleteStorage).toHaveBeenCalledWith(key);
|
||||
expect(initialize).toHaveBeenCalledOnce();
|
||||
expect(mockSetStorage).toHaveBeenCalledOnce();
|
||||
expect(mockSetStorage).toHaveBeenCalledWith(key, { new: 'state' });
|
||||
expect(navigationApi.isPanelRegistered(tab, 'p')).toBe(true);
|
||||
});
|
||||
|
||||
it('persists on layout change after debounce', () => {
|
||||
vi.useFakeTimers();
|
||||
mockGetStorage.mockReturnValue(undefined);
|
||||
const initialize = vi.fn();
|
||||
const panel = { id: 'p' };
|
||||
let layoutCb: () => void = () => {};
|
||||
const mockApi = {
|
||||
panels: [panel],
|
||||
toJSON: vi.fn(() => ({ x: 1 })),
|
||||
onDidLayoutChange: vi.fn((cb) => {
|
||||
layoutCb = cb;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as unknown as DockviewApi | GridviewApi;
|
||||
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
|
||||
|
||||
// first set: initial persistence
|
||||
expect(mockSetStorage).toHaveBeenCalledWith(key, { x: 1 });
|
||||
|
||||
// simulate layout change
|
||||
layoutCb();
|
||||
// advance past debounce (300ms)
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(mockSetStorage).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetStorage).toHaveBeenLastCalledWith(key, { x: 1 });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does nothing if app not connected', () => {
|
||||
navigationApi.disconnectFromApp();
|
||||
const initialize = vi.fn();
|
||||
const mockApi = {
|
||||
panels: [],
|
||||
fromJSON: vi.fn(),
|
||||
toJSON: vi.fn(),
|
||||
onDidLayoutChange: vi.fn(),
|
||||
} as unknown as DockviewApi | GridviewApi;
|
||||
expect(() => navigationApi.registerContainer(tab, viewId, mockApi, initialize)).not.toThrow();
|
||||
expect(mockGetStorage).not.toHaveBeenCalled();
|
||||
expect(initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { createDeferredPromise, type Deferred } from 'common/util/createDeferredPromise';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { DockviewApi, GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview';
|
||||
import { GridviewPanel } from 'dockview';
|
||||
import { DockviewPanel, GridviewPanel, type IDockviewPanel, type IGridviewPanel } from 'dockview';
|
||||
import { debounce } from 'es-toolkit';
|
||||
import type { Serializable, TabName } from 'features/ui/store/uiTypes';
|
||||
import type { Atom } from 'nanostores';
|
||||
import type { StoredDockviewPanelState, StoredGridviewPanelState, TabName } from 'features/ui/store/uiTypes';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
import {
|
||||
@@ -30,23 +27,14 @@ type Waiter = {
|
||||
timeoutId: ReturnType<typeof setTimeout> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The API exposed by the application to manage navigation and panel states.
|
||||
*/
|
||||
export type NavigationAppApi = {
|
||||
/**
|
||||
* API to manage the currently active tab in the application.
|
||||
*/
|
||||
activeTab: {
|
||||
get: () => TabName;
|
||||
set: (tab: TabName) => void;
|
||||
};
|
||||
/**
|
||||
* API to manage the storage of panel states.
|
||||
*/
|
||||
storage: {
|
||||
get: (id: string) => Serializable | undefined;
|
||||
set: (id: string, state: Serializable) => void;
|
||||
panelStorage: {
|
||||
get: (id: string) => StoredDockviewPanelState | StoredGridviewPanelState | undefined;
|
||||
set: (id: string, state: StoredDockviewPanelState | StoredGridviewPanelState) => void;
|
||||
delete: (id: string) => void;
|
||||
};
|
||||
};
|
||||
@@ -66,17 +54,20 @@ export class NavigationApi {
|
||||
/**
|
||||
* A flag indicating if the application is currently switching tabs, which can take some time.
|
||||
*/
|
||||
private _$isLoading = atom(false);
|
||||
$isLoading: Atom<boolean> = this._$isLoading;
|
||||
$isSwitchingTabs = atom(false);
|
||||
/**
|
||||
* The timeout used to add a short additional delay when switching tabs.
|
||||
*
|
||||
* The time it takes to switch tabs varies depending on the tab, and sometimes it is very fast, resulting in a flicker
|
||||
* of the loading screen. This timeout is used to artificially extend the time the loading screen is shown.
|
||||
*/
|
||||
switchingTabsTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/**
|
||||
* Separator used to create unique keys for panels. Typo protection.
|
||||
*/
|
||||
KEY_SEPARATOR = ':';
|
||||
|
||||
/**
|
||||
* The application API that provides methods to set and get the current app tab and manage panel storage.
|
||||
*/
|
||||
_app: NavigationAppApi | null = null;
|
||||
|
||||
/**
|
||||
@@ -95,46 +86,154 @@ export class NavigationApi {
|
||||
this._app = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the flag indicating that the navigation is loading and schedules a debounced hide of the loading screen.
|
||||
*/
|
||||
_showFakeLoadingScreen = () => {
|
||||
log.trace('Showing fake loading screen for tab switch');
|
||||
this._$isLoading.set(true);
|
||||
this._hideLoadingScreenDebounced();
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounced function to hide the loading screen after a delay.
|
||||
*/
|
||||
_hideLoadingScreenDebounced = debounce(() => {
|
||||
log.trace('Hiding fake loading screen for tab switch');
|
||||
this._$isLoading.set(false);
|
||||
}, SWITCH_TABS_FAKE_DELAY_MS);
|
||||
|
||||
/**
|
||||
* Switch to a specific app tab.
|
||||
*
|
||||
* The loading screen will be shown while the tab is switching (and for a little while longer to smooth out the UX).
|
||||
* The loading screen will be shown while the tab is switching.
|
||||
*
|
||||
* @param tab - The tab to switch to
|
||||
* @return True if the switch was successful, false otherwise
|
||||
*/
|
||||
switchToTab = (tab: TabName): boolean => {
|
||||
if (!this._app) {
|
||||
log.error('No app connected to switch tabs');
|
||||
return false;
|
||||
if (this.switchingTabsTimeout !== null) {
|
||||
clearTimeout(this.switchingTabsTimeout);
|
||||
this.switchingTabsTimeout = null;
|
||||
}
|
||||
|
||||
if (tab === this._app.activeTab.get()) {
|
||||
log.trace(`Already on tab: ${tab}`);
|
||||
if (tab === this._app?.activeTab.get?.()) {
|
||||
return true;
|
||||
}
|
||||
this.$isSwitchingTabs.set(true);
|
||||
log.debug(`Switching to tab: ${tab}`);
|
||||
if (this._app) {
|
||||
this._app.activeTab.set(tab);
|
||||
return true;
|
||||
} else {
|
||||
log.error('No setAppTab function available to switch tabs');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
log.trace(`Switching to tab: ${tab}`);
|
||||
this._showFakeLoadingScreen();
|
||||
this._app.activeTab.set(tab);
|
||||
return true;
|
||||
/**
|
||||
* Callback for when a tab is ready after switching.
|
||||
*
|
||||
* Hides the loading screen after a short delay.
|
||||
*/
|
||||
onTabReady = (tab: TabName): void => {
|
||||
this.switchingTabsTimeout = setTimeout(() => {
|
||||
this.$isSwitchingTabs.set(false);
|
||||
log.debug(`Tab ${tab} ready`);
|
||||
}, SWITCH_TABS_FAKE_DELAY_MS);
|
||||
};
|
||||
|
||||
_initGridviewPanelStorage = (key: string, panel: IGridviewPanel) => {
|
||||
if (!this._app) {
|
||||
log.error('App not connected');
|
||||
return;
|
||||
}
|
||||
const storedState = this._app.panelStorage.get(key);
|
||||
if (!storedState) {
|
||||
log.debug('No stored state for panel, setting initial state');
|
||||
const { height, width } = panel.api;
|
||||
this._app.panelStorage.set(key, {
|
||||
id: key,
|
||||
type: 'gridview-panel',
|
||||
dimensions: { height, width },
|
||||
});
|
||||
} else {
|
||||
if (storedState.type !== 'gridview-panel') {
|
||||
log.error(`Panel ${key} type mismatch: expected gridview-panel, got ${storedState.type}`);
|
||||
this._app.panelStorage.delete(key);
|
||||
return;
|
||||
}
|
||||
log.debug({ storedState }, 'Found stored state for panel, restoring');
|
||||
|
||||
// If the panel's dimensions are 0, we assume it was collapsed by the user. But when panels are initialzed,
|
||||
// by default they may have a minimize dimension greater than 0. If we attempt to set a size of 0, it will
|
||||
// not work - dockview will instead set the size to the minimum size.
|
||||
//
|
||||
// The user-facing issue is that the panel will not remember if it was collapsed or not, and will always
|
||||
// be expanded when navigating to the tab.
|
||||
//
|
||||
// To fix this, if we find a stored state with dimensions of 0, we set the constraints to 0 before setting the
|
||||
// size.
|
||||
if (storedState.dimensions.width === 0) {
|
||||
panel.api.setConstraints({ minimumWidth: 0, maximumWidth: 0 });
|
||||
}
|
||||
|
||||
if (storedState.dimensions.height === 0) {
|
||||
panel.api.setConstraints({ minimumHeight: 0, maximumHeight: 0 });
|
||||
}
|
||||
|
||||
panel.api.setSize(storedState.dimensions);
|
||||
}
|
||||
const { dispose } = panel.api.onDidDimensionsChange(
|
||||
debounce(({ width, height }) => {
|
||||
log.debug({ key, width, height }, 'Panel dimensions changed');
|
||||
if (!this._app) {
|
||||
log.error('App not connected');
|
||||
return;
|
||||
}
|
||||
this._app.panelStorage.set(key, {
|
||||
id: key,
|
||||
type: 'gridview-panel',
|
||||
dimensions: { width, height },
|
||||
});
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
return dispose;
|
||||
};
|
||||
|
||||
_initDockviewPanelStorage = (key: string, panel: IDockviewPanel) => {
|
||||
if (!this._app) {
|
||||
log.error('App not connected');
|
||||
return;
|
||||
}
|
||||
const storedState = this._app.panelStorage.get(key);
|
||||
if (!storedState) {
|
||||
const { isActive } = panel.api;
|
||||
this._app.panelStorage.set(key, {
|
||||
id: key,
|
||||
type: 'dockview-panel',
|
||||
isActive,
|
||||
});
|
||||
} else {
|
||||
if (storedState.type !== 'dockview-panel') {
|
||||
log.error(`Panel ${key} type mismatch: expected dockview-panel, got ${storedState.type}`);
|
||||
this._app.panelStorage.delete(key);
|
||||
return;
|
||||
}
|
||||
if (storedState.isActive) {
|
||||
panel.api.setActive();
|
||||
}
|
||||
}
|
||||
|
||||
const { dispose } = panel.api.onDidActiveChange(
|
||||
debounce(({ isActive }) => {
|
||||
if (!this._app) {
|
||||
log.error('App not connected');
|
||||
return;
|
||||
}
|
||||
this._app.panelStorage.set(key, {
|
||||
id: key,
|
||||
type: 'dockview-panel',
|
||||
isActive,
|
||||
});
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
return dispose;
|
||||
};
|
||||
|
||||
_initPanelStorage = (key: string, panel: PanelType) => {
|
||||
if (panel instanceof GridviewPanel) {
|
||||
return this._initGridviewPanelStorage(key, panel);
|
||||
} else if (panel instanceof DockviewPanel) {
|
||||
return this._initDockviewPanelStorage(key, panel);
|
||||
} else {
|
||||
log.error(`Unsupported panel type: ${panel.constructor.name}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -145,11 +244,13 @@ export class NavigationApi {
|
||||
* @param panel - The panel instance
|
||||
* @returns Cleanup function to unregister the panel
|
||||
*/
|
||||
_registerPanel = <T extends PanelType>(tab: TabName, panelId: string, panel: T): (() => void) => {
|
||||
registerPanel = (tab: TabName, panelId: string, panel: PanelType): (() => void) => {
|
||||
const key = this._getPanelKey(tab, panelId);
|
||||
|
||||
this.panels.set(key, panel);
|
||||
|
||||
const cleanupPanelStorage = this._initPanelStorage(key, panel);
|
||||
|
||||
// Resolve any pending waiters for this panel, notifying them that the panel is now registered.
|
||||
const waiter = this.waiters.get(key);
|
||||
if (waiter) {
|
||||
@@ -160,64 +261,15 @@ export class NavigationApi {
|
||||
this.waiters.delete(key);
|
||||
}
|
||||
|
||||
log.trace(`Registered panel ${key}`);
|
||||
log.debug(`Registered panel ${key}`);
|
||||
|
||||
return () => {
|
||||
cleanupPanelStorage?.();
|
||||
this.panels.delete(key);
|
||||
log.trace(`Unregistered panel ${key}`);
|
||||
log.debug(`Unregistered panel ${key}`);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a container (Dockview or Gridview) with the navigation API.
|
||||
*
|
||||
* This method initializes the container from storage if available, or calls the provided initialize function
|
||||
* to set it up from scratch.
|
||||
*
|
||||
* @param tab - The tab this container belongs to
|
||||
* @param id - Unique identifier for the container
|
||||
* @param api - The DockviewApi or GridviewApi instance
|
||||
* @param initialize - Function to call if the container needs to be initialized from scratch
|
||||
*/
|
||||
registerContainer = (tab: TabName, id: string, api: DockviewApi | GridviewApi, initialize: () => void) => {
|
||||
if (!this._app) {
|
||||
log.error('App not connected to register view');
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this._getContainerKey(tab, id);
|
||||
|
||||
const stored = this._app.storage.get(key);
|
||||
if (stored) {
|
||||
try {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
api.fromJSON(stored as any);
|
||||
log.trace({ stored: parseify(stored) }, `Restored view ${key} from storage`);
|
||||
} catch (error) {
|
||||
log.error({ error: parseify(error) }, `Failed to restore view ${key} from storage`);
|
||||
this._app.storage.delete(key);
|
||||
initialize();
|
||||
this._app.storage.set(key, api.toJSON());
|
||||
}
|
||||
} else {
|
||||
initialize();
|
||||
log.trace(`Initialized ${key} from scratch`);
|
||||
this._app.storage.set(key, api.toJSON());
|
||||
}
|
||||
|
||||
for (const panel of api.panels) {
|
||||
this._registerPanel(tab, panel.id, panel);
|
||||
}
|
||||
|
||||
api.onDidLayoutChange(
|
||||
debounce(() => {
|
||||
this._app?.storage.set(key, api.toJSON());
|
||||
}, 300)
|
||||
);
|
||||
|
||||
log.trace(`Registered view ${key}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for a panel to be ready.
|
||||
*
|
||||
@@ -267,38 +319,17 @@ export class NavigationApi {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the prefix for a tab to create unique keys for panels/containers.
|
||||
* Get the prefix for a tab to create unique keys for panels.
|
||||
*/
|
||||
_getTabPrefix = (tab: TabName): string => {
|
||||
return `${tab}${this.KEY_SEPARATOR}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a prefix for a panel based on its tab.
|
||||
*/
|
||||
_getPanelPrefix = (tab: TabName): string => {
|
||||
return `${this._getTabPrefix(tab)}panel${this.KEY_SEPARATOR}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the unique key for a panel based on its tab and ID.
|
||||
*/
|
||||
_getPanelKey = (tab: TabName, panelId: string): string => {
|
||||
return `${this._getPanelPrefix(tab)}${panelId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a prefix for a container based on its tab.
|
||||
*/
|
||||
_getContainerPrefix = (tab: TabName): string => {
|
||||
return `${this._getTabPrefix(tab)}container${this.KEY_SEPARATOR}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the unique key for a container based on its tab and ID.
|
||||
*/
|
||||
_getContainerKey = (tab: TabName, viewId: string): string => {
|
||||
return `${this._getContainerPrefix(tab)}${viewId}`;
|
||||
return `${this._getTabPrefix(tab)}${panelId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -337,7 +368,7 @@ export class NavigationApi {
|
||||
|
||||
// Dockview uses the term "active", but we use "focused" for consistency.
|
||||
panel.api.setActive();
|
||||
log.trace(`Focused panel ${key}`);
|
||||
log.debug(`Focused panel ${key}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -427,7 +458,7 @@ export class NavigationApi {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCollapsed = leftPanel.width === 0;
|
||||
const isCollapsed = leftPanel.maximumWidth === 0;
|
||||
if (isCollapsed) {
|
||||
this._expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
@@ -460,7 +491,7 @@ export class NavigationApi {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCollapsed = rightPanel.width === 0;
|
||||
const isCollapsed = rightPanel.maximumWidth === 0;
|
||||
if (isCollapsed) {
|
||||
this._expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
@@ -496,8 +527,8 @@ export class NavigationApi {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLeftCollapsed = leftPanel.width === 0;
|
||||
const isRightCollapsed = rightPanel.width === 0;
|
||||
const isLeftCollapsed = leftPanel.maximumWidth === 0;
|
||||
const isRightCollapsed = rightPanel.maximumWidth === 0;
|
||||
|
||||
if (isLeftCollapsed || isRightCollapsed) {
|
||||
this._expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX);
|
||||
@@ -562,7 +593,7 @@ export class NavigationApi {
|
||||
* @returns Array of panel IDs
|
||||
*/
|
||||
getRegisteredPanels = (tab: TabName): string[] => {
|
||||
const prefix = this._getPanelPrefix(tab);
|
||||
const prefix = this._getTabPrefix(tab);
|
||||
return Array.from(this.panels.keys())
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length));
|
||||
@@ -573,7 +604,7 @@ export class NavigationApi {
|
||||
* @param tab - The tab to unregister panels for
|
||||
*/
|
||||
unregisterTab = (tab: TabName): void => {
|
||||
const prefix = this._getPanelPrefix(tab);
|
||||
const prefix = this._getTabPrefix(tab);
|
||||
const keysToDelete = Array.from(this.panels.keys()).filter((key) => key.startsWith(prefix));
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
@@ -593,7 +624,7 @@ export class NavigationApi {
|
||||
this.waiters.delete(key);
|
||||
}
|
||||
|
||||
log.trace(`Unregistered all panels for tab ${tab}`);
|
||||
log.debug(`Unregistered all panels for tab ${tab}`);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { GridviewApi, IGridviewReactProps } from 'dockview';
|
||||
import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview';
|
||||
import { GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||
import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import { navigationApi } from './navigation-api';
|
||||
@@ -13,19 +12,22 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[QUEUE_PANEL_ID]: QueueTab,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
api.addPanel({
|
||||
id: QUEUE_PANEL_ID,
|
||||
component: QUEUE_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const queue = layoutApi.addPanel({
|
||||
id: QUEUE_PANEL_ID,
|
||||
component: QUEUE_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
navigationApi.registerPanel('queue', QUEUE_PANEL_ID, queue);
|
||||
|
||||
return { queue } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const QueueTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('queue', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('queue');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
|
||||
import type {
|
||||
DockviewApi,
|
||||
GridviewApi,
|
||||
IDockviewPanel,
|
||||
IDockviewReactProps,
|
||||
IGridviewPanel,
|
||||
IGridviewReactProps,
|
||||
} from 'dockview';
|
||||
import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import { UpscalingLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel';
|
||||
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
|
||||
@@ -57,42 +64,46 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
|
||||
};
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
launchpad.api.setActive();
|
||||
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const MainPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeMainPanelLayout(tab, api);
|
||||
initializeCenterPanelLayout(tab, api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -122,35 +133,39 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX });
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
@@ -178,16 +193,19 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
@@ -217,42 +235,47 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const main = layoutApi.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = layoutApi.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = layoutApi.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('upscaling', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('upscaling', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('upscaling', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const UpscalingTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('upscaling', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('upscaling');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -11,9 +11,9 @@ const getIsCollapsed = (
|
||||
collapsedSize?: number
|
||||
) => {
|
||||
if (orientation === 'vertical') {
|
||||
return panel.height <= (collapsedSize ?? panel.minimumHeight ?? 0);
|
||||
return panel.height <= (collapsedSize ?? panel.minimumHeight);
|
||||
}
|
||||
return panel.width <= (collapsedSize ?? panel.minimumWidth ?? 0);
|
||||
return panel.width <= (collapsedSize ?? panel.minimumWidth);
|
||||
};
|
||||
|
||||
export const useCollapsibleGridviewPanel = (
|
||||
@@ -36,9 +36,9 @@ export const useCollapsibleGridviewPanel = (
|
||||
lastExpandedSizeRef.current = orientation === 'vertical' ? panel.height : panel.width;
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight ?? 0 });
|
||||
panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight });
|
||||
} else {
|
||||
panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth ?? 0 });
|
||||
panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
|
||||
}
|
||||
}, [collapsedSize, orientation, panelId, tab]);
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { dockviewStorageKeyChanged, setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { panelStateChanged, setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { StoredDockviewPanelState, StoredGridviewPanelState, TabName } from 'features/ui/store/uiTypes';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
import { navigationApi } from './navigation-api';
|
||||
|
||||
@@ -26,15 +25,15 @@ export const useNavigationApi = () => {
|
||||
store.dispatch(setActiveTab(tab));
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
panelStorage: {
|
||||
get: (id: string) => {
|
||||
return store.getState().ui.panels[id];
|
||||
},
|
||||
set: (id: string, state: JsonObject) => {
|
||||
store.dispatch(dockviewStorageKeyChanged({ id, state }));
|
||||
set: (id: string, state: StoredDockviewPanelState | StoredGridviewPanelState) => {
|
||||
store.dispatch(panelStateChanged({ id, state }));
|
||||
},
|
||||
delete: (id: string) => {
|
||||
store.dispatch(dockviewStorageKeyChanged({ id, state: undefined }));
|
||||
store.dispatch(panelStateChanged({ id, state: undefined }));
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
|
||||
import type {
|
||||
DockviewApi,
|
||||
GridviewApi,
|
||||
IDockviewPanel,
|
||||
IDockviewReactProps,
|
||||
IGridviewPanel,
|
||||
IGridviewReactProps,
|
||||
} from 'dockview';
|
||||
import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview';
|
||||
import { WorkflowsLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/WorkflowsLaunchpadPanel';
|
||||
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
|
||||
@@ -61,50 +68,54 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
};
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Workflow Editor',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'workflows',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Workflow Editor',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'workflows',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
launchpad.api.setActive();
|
||||
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, WORKSPACE_PANEL_ID, workspace);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const MainPanel = memo(() => {
|
||||
@@ -142,35 +153,39 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX });
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
@@ -198,16 +213,19 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
};
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
@@ -236,42 +254,45 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: MAIN_PANEL_ID,
|
||||
},
|
||||
});
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: MAIN_PANEL_ID,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('workflows', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('workflows', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('workflows', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const WorkflowsTabAutoLayout = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
initializeRootPanelLayout('workflows', api);
|
||||
initializeRootPanelLayout(api);
|
||||
navigationApi.onTabReady('workflows');
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
||||
@@ -51,7 +51,7 @@ export const uiSlice = createSlice({
|
||||
const { id, size } = action.payload;
|
||||
state.textAreaSizes[id] = size;
|
||||
},
|
||||
dockviewStorageKeyChanged: (
|
||||
panelStateChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: keyof UIState['panels'];
|
||||
@@ -80,7 +80,7 @@ export const {
|
||||
expanderStateChanged,
|
||||
shouldShowNotificationChanged,
|
||||
textAreaSizesStateChanged,
|
||||
dockviewStorageKeyChanged,
|
||||
panelStateChanged,
|
||||
} = uiSlice.actions;
|
||||
|
||||
export const selectUiSlice = (state: RootState) => state.ui;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { isPlainObject } from 'es-toolkit';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
|
||||
@@ -11,19 +10,35 @@ const zPartialDimensions = z.object({
|
||||
height: z.number().optional(),
|
||||
});
|
||||
|
||||
const zSerializable = z.any().refine(isPlainObject);
|
||||
export type Serializable = z.infer<typeof zSerializable>;
|
||||
const zDimensions = z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
});
|
||||
|
||||
const zDockviewPanelState = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('dockview-panel'),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
export type StoredDockviewPanelState = z.infer<typeof zDockviewPanelState>;
|
||||
|
||||
const zGridviewPanelState = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('gridview-panel'),
|
||||
dimensions: zDimensions,
|
||||
});
|
||||
export type StoredGridviewPanelState = z.infer<typeof zGridviewPanelState>;
|
||||
|
||||
const zUIState = z.object({
|
||||
_version: z.literal(3).default(3),
|
||||
activeTab: zTabName.default('generate'),
|
||||
activeTab: zTabName.default('canvas'),
|
||||
activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'),
|
||||
shouldShowImageDetails: z.boolean().default(false),
|
||||
shouldShowProgressInViewer: z.boolean().default(true),
|
||||
accordions: z.record(z.string(), z.boolean()).default(() => ({})),
|
||||
expanders: z.record(z.string(), z.boolean()).default(() => ({})),
|
||||
textAreaSizes: z.record(z.string(), zPartialDimensions).default({}),
|
||||
panels: z.record(z.string(), zSerializable).default({}),
|
||||
panels: z.record(z.string(), z.discriminatedUnion('type', [zDockviewPanelState, zGridviewPanelState])).default({}),
|
||||
shouldShowNotificationV2: z.boolean().default(true),
|
||||
});
|
||||
const INITIAL_STATE = zUIState.parse({});
|
||||
|
||||
@@ -263,6 +263,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
invalidatesTags: (result) => {
|
||||
if (!result || result.is_intermediate) {
|
||||
// Don't add it to anything
|
||||
@@ -275,7 +276,6 @@ export const imagesApi = api.injectEndpoints({
|
||||
...getTagsToInvalidateForBoardAffectingMutation([boardId]),
|
||||
'ImageCollectionCounts',
|
||||
{ type: 'ImageCollection', id: LIST_TAG },
|
||||
'ImageNameList',
|
||||
];
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -129,17 +129,6 @@ export const selectIPAdapterModels = buildModelsSelector(isIPAdapterModelConfig)
|
||||
// export const selectEmbeddingModels = buildModelsSelector(isTIModelConfig);
|
||||
// export const selectVAEModels = buildModelsSelector(isVAEModelConfig);
|
||||
// export const selectFluxVAEModels = buildModelsSelector(isFluxVAEModelConfig);
|
||||
export const selectGlobalRefImageModels = buildModelsSelector(
|
||||
(config) =>
|
||||
isIPAdapterModelConfig(config) ||
|
||||
isFluxReduxModelConfig(config) ||
|
||||
isChatGPT4oModelConfig(config) ||
|
||||
isFluxKontextApiModelConfig(config) ||
|
||||
isFluxKontextModelConfig(config)
|
||||
);
|
||||
export const selectRegionalRefImageModels = buildModelsSelector(
|
||||
(config) => isIPAdapterModelConfig(config) || isFluxReduxModelConfig(config)
|
||||
);
|
||||
|
||||
export const buildSelectModelConfig = <T extends AnyModelConfig>(
|
||||
key: string,
|
||||
|
||||
@@ -22266,6 +22266,16 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
/** @example {
|
||||
* "path": "/path/to/model",
|
||||
* "name": "model_name",
|
||||
* "base": "sd-1",
|
||||
* "type": "main",
|
||||
* "format": "checkpoint",
|
||||
* "config_path": "configs/stable-diffusion/v1-inference.yaml",
|
||||
* "description": "Model description",
|
||||
* "variant": "normal"
|
||||
* } */
|
||||
"application/json": components["schemas"]["ModelRecordChanges"];
|
||||
};
|
||||
};
|
||||
@@ -22569,6 +22579,10 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
/** @example {
|
||||
* "name": "string",
|
||||
* "description": "string"
|
||||
* } */
|
||||
"application/json": components["schemas"]["ModelRecordChanges"];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "6.0.0"
|
||||
__version__ = "6.0.0rc5"
|
||||
|
||||
Reference in New Issue
Block a user