mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
[ui]: add resizable pinnable drawer component (#2874)
wip this is based off the branch in #2873
This commit is contained in:
commit
2eef6df66a
@ -35,6 +35,7 @@ module.exports = {
|
||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||
],
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -1 +0,0 @@
|
||||
.ltr-image-gallery-css-transition-enter{transform:translate(150%)}.ltr-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-image-gallery-css-transition-exit{transform:translate(0)}.ltr-image-gallery-css-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}.rtl-image-gallery-css-transition-enter{transform:translate(-150%)}.rtl-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-image-gallery-css-transition-exit{transform:translate(0)}.rtl-image-gallery-css-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}.ltr-parameters-panel-transition-enter{transform:translate(-150%)}.ltr-parameters-panel-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-parameters-panel-transition-exit{transform:translate(0)}.ltr-parameters-panel-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}.rtl-parameters-panel-transition-enter{transform:translate(150%)}.rtl-parameters-panel-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-parameters-panel-transition-exit{transform:translate(0)}.rtl-parameters-panel-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}
|
1
invokeai/frontend/web/dist/assets/App-08e5c546.css
vendored
Normal file
1
invokeai/frontend/web/dist/assets/App-08e5c546.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.ltr-image-gallery-css-transition-enter{transform:translate(150%)}.ltr-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-image-gallery-css-transition-exit{transform:translate(0)}.ltr-image-gallery-css-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}.rtl-image-gallery-css-transition-enter{transform:translate(-150%)}.rtl-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-image-gallery-css-transition-exit{transform:translate(0)}.rtl-image-gallery-css-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}
|
188
invokeai/frontend/web/dist/assets/App-5c94d6ff.js
vendored
188
invokeai/frontend/web/dist/assets/App-5c94d6ff.js
vendored
File diff suppressed because one or more lines are too long
188
invokeai/frontend/web/dist/assets/App-b40e839f.js
vendored
Normal file
188
invokeai/frontend/web/dist/assets/App-b40e839f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
115
invokeai/frontend/web/dist/assets/index-c5a5b67c.js
vendored
115
invokeai/frontend/web/dist/assets/index-c5a5b67c.js
vendored
File diff suppressed because one or more lines are too long
115
invokeai/frontend/web/dist/assets/index-e1f916bd.js
vendored
Normal file
115
invokeai/frontend/web/dist/assets/index-e1f916bd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
9
invokeai/frontend/web/dist/assets/storeHooks-548a355c.js
vendored
Normal file
9
invokeai/frontend/web/dist/assets/storeHooks-548a355c.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
invokeai/frontend/web/dist/index.html
vendored
2
invokeai/frontend/web/dist/index.html
vendored
@ -12,7 +12,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/index-c5a5b67c.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-e1f916bd.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index-5483945c.css">
|
||||
</head>
|
||||
|
||||
|
5
invokeai/frontend/web/dist/locales/en.json
vendored
5
invokeai/frontend/web/dist/locales/en.json
vendored
@ -49,10 +49,11 @@
|
||||
"langSimplifiedChinese": "简体中文",
|
||||
"langUkranian": "Украї́нська",
|
||||
"langSpanish": "Español",
|
||||
"text2img": "Text To Image",
|
||||
"txt2img": "Text To Image",
|
||||
"img2img": "Image To Image",
|
||||
"unifiedCanvas": "Unified Canvas",
|
||||
"nodes": "Nodes",
|
||||
"postprocessing": "Post Processing",
|
||||
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
|
||||
"postProcessing": "Post Processing",
|
||||
"postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.",
|
||||
@ -596,7 +597,7 @@
|
||||
"autoSaveToGallery": "Auto Save to Gallery",
|
||||
"saveBoxRegionOnly": "Save Box Region Only",
|
||||
"limitStrokesToBox": "Limit Strokes to Box",
|
||||
"showCanvasDebugInfo": "Show Canvas Debug Info",
|
||||
"showCanvasDebugInfo": "Show Additional Canvas Info",
|
||||
"clearCanvasHistory": "Clear Canvas History",
|
||||
"clearHistory": "Clear History",
|
||||
"clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.",
|
||||
|
40
invokeai/frontend/web/dist/locales/es.json
vendored
40
invokeai/frontend/web/dist/locales/es.json
vendored
@ -63,7 +63,14 @@
|
||||
"back": "Atrás",
|
||||
"statusConvertingModel": "Convertir el modelo",
|
||||
"statusModelConverted": "Modelo adaptado",
|
||||
"statusMergingModels": "Fusionar modelos"
|
||||
"statusMergingModels": "Fusionar modelos",
|
||||
"oceanTheme": "Océano",
|
||||
"langPortuguese": "Portugués",
|
||||
"langKorean": "Coreano",
|
||||
"langHebrew": "Hebreo",
|
||||
"pinOptionsPanel": "Pin del panel de opciones",
|
||||
"loading": "Cargando",
|
||||
"loadingInvokeAI": "Cargando invocar a la IA"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Generaciones",
|
||||
@ -385,14 +392,19 @@
|
||||
"modelMergeAlphaHelp": "Alfa controla la fuerza de mezcla de los modelos. Los valores alfa más bajos reducen la influencia del segundo modelo.",
|
||||
"modelMergeInterpAddDifferenceHelp": "En este modo, el Modelo 3 se sustrae primero del Modelo 2. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente.",
|
||||
"ignoreMismatch": "Ignorar discrepancias entre modelos seleccionados",
|
||||
"modelMergeHeaderHelp1": "Puede combinar hasta tres modelos diferentes para crear una mezcla que se adapte a sus necesidades.",
|
||||
"modelMergeHeaderHelp1": "Puede unir hasta tres modelos diferentes para crear una combinación que se adapte a sus necesidades.",
|
||||
"inverseSigmoid": "Sigmoideo inverso",
|
||||
"weightedSum": "Modelo de suma ponderada",
|
||||
"sigmoid": "Función sigmoide",
|
||||
"allModels": "Todos los modelos",
|
||||
"repo_id": "Identificador del repositorio",
|
||||
"pathToCustomConfig": "Ruta a la configuración personalizada",
|
||||
"customConfig": "Configuración personalizada"
|
||||
"customConfig": "Configuración personalizada",
|
||||
"v2_base": "v2 (512px)",
|
||||
"none": "ninguno",
|
||||
"pickModelType": "Elige el tipo de modelo",
|
||||
"v2_768": "v2 (768px)",
|
||||
"addDifference": "Añadir una diferencia"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Imágenes",
|
||||
@ -588,5 +600,27 @@
|
||||
"betaDarkenOutside": "Oscurecer fuera",
|
||||
"betaLimitToBox": "Limitar a caja",
|
||||
"betaPreserveMasked": "Preservar área enmascarada"
|
||||
},
|
||||
"accessibility": {
|
||||
"invokeProgressBar": "Activar la barra de progreso",
|
||||
"modelSelect": "Seleccionar modelo",
|
||||
"reset": "Reiniciar",
|
||||
"uploadImage": "Cargar imagen",
|
||||
"previousImage": "Imagen anterior",
|
||||
"nextImage": "Siguiente imagen",
|
||||
"useThisParameter": "Utiliza este parámetro",
|
||||
"copyMetadataJson": "Copiar los metadatos JSON",
|
||||
"exitViewer": "Salir del visor",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar",
|
||||
"rotateCounterClockwise": "Girar en sentido antihorario",
|
||||
"rotateClockwise": "Girar en sentido horario",
|
||||
"flipHorizontally": "Voltear horizontalmente",
|
||||
"flipVertically": "Voltear verticalmente",
|
||||
"modifyConfig": "Modificar la configuración",
|
||||
"toggleAutoscroll": "Activar el autodesplazamiento",
|
||||
"toggleLogViewer": "Alternar el visor de registros",
|
||||
"showGallery": "Mostrar galería",
|
||||
"showOptionsPanel": "Mostrar el panel de opciones"
|
||||
}
|
||||
}
|
||||
|
38
invokeai/frontend/web/dist/locales/it.json
vendored
38
invokeai/frontend/web/dist/locales/it.json
vendored
@ -63,7 +63,14 @@
|
||||
"langSimplifiedChinese": "Cinese semplificato",
|
||||
"langDutch": "Olandese",
|
||||
"statusModelConverted": "Modello Convertito",
|
||||
"statusConvertingModel": "Conversione Modello"
|
||||
"statusConvertingModel": "Conversione Modello",
|
||||
"langKorean": "Coreano",
|
||||
"langPortuguese": "Portoghese",
|
||||
"pinOptionsPanel": "Blocca il pannello Opzioni",
|
||||
"loading": "Caricamento in corso",
|
||||
"oceanTheme": "Oceano",
|
||||
"langHebrew": "Ebraico",
|
||||
"loadingInvokeAI": "Caricamento Invoke AI"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Generazioni",
|
||||
@ -392,7 +399,12 @@
|
||||
"customSaveLocation": "Ubicazione salvataggio personalizzata",
|
||||
"weightedSum": "Somma pesata",
|
||||
"sigmoid": "Sigmoide",
|
||||
"inverseSigmoid": "Sigmoide inverso"
|
||||
"inverseSigmoid": "Sigmoide inverso",
|
||||
"v2_base": "v2 (512px)",
|
||||
"v2_768": "v2 (768px)",
|
||||
"none": "niente",
|
||||
"addDifference": "Aggiungi differenza",
|
||||
"pickModelType": "Scegli il tipo di modello"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Immagini",
|
||||
@ -588,5 +600,27 @@
|
||||
"betaDarkenOutside": "Oscura all'esterno",
|
||||
"betaLimitToBox": "Limita al rettangolo",
|
||||
"betaPreserveMasked": "Conserva quanto mascherato"
|
||||
},
|
||||
"accessibility": {
|
||||
"modelSelect": "Seleziona modello",
|
||||
"invokeProgressBar": "Barra di avanzamento generazione",
|
||||
"uploadImage": "Carica immagine",
|
||||
"previousImage": "Immagine precedente",
|
||||
"nextImage": "Immagine successiva",
|
||||
"useThisParameter": "Usa questo parametro",
|
||||
"reset": "Reimposta",
|
||||
"copyMetadataJson": "Copia i metadati JSON",
|
||||
"exitViewer": "Esci dal visualizzatore",
|
||||
"zoomIn": "Zoom avanti",
|
||||
"zoomOut": "Zoom Indietro",
|
||||
"rotateCounterClockwise": "Ruotare in senso antiorario",
|
||||
"rotateClockwise": "Ruotare in senso orario",
|
||||
"flipHorizontally": "Capovolgi orizzontalmente",
|
||||
"toggleLogViewer": "Attiva/disattiva visualizzatore registro",
|
||||
"showGallery": "Mostra la galleria immagini",
|
||||
"showOptionsPanel": "Mostra il pannello opzioni",
|
||||
"flipVertically": "Capovolgi verticalmente",
|
||||
"toggleAutoscroll": "Attiva/disattiva lo scorrimento automatico",
|
||||
"modifyConfig": "Modifica configurazione"
|
||||
}
|
||||
}
|
||||
|
556
invokeai/frontend/web/dist/locales/pt.json
vendored
556
invokeai/frontend/web/dist/locales/pt.json
vendored
@ -63,6 +63,560 @@
|
||||
"statusGeneratingOutpainting": "Geração de Ampliação",
|
||||
"statusGenerationComplete": "Geração Completa",
|
||||
"statusMergingModels": "Mesclando Modelos",
|
||||
"statusMergedModels": "Modelos Mesclados"
|
||||
"statusMergedModels": "Modelos Mesclados",
|
||||
"oceanTheme": "Oceano",
|
||||
"pinOptionsPanel": "Fixar painel de opções",
|
||||
"loading": "A carregar",
|
||||
"loadingInvokeAI": "A carregar Invoke AI",
|
||||
"langPortuguese": "Português"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageResetSize": "Resetar Imagem",
|
||||
"gallerySettings": "Configurações de Galeria",
|
||||
"maintainAspectRatio": "Mater Proporções",
|
||||
"autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente",
|
||||
"pinGallery": "Fixar Galeria",
|
||||
"singleColumnLayout": "Disposição em Coluna Única",
|
||||
"allImagesLoaded": "Todas as Imagens Carregadas",
|
||||
"loadMore": "Carregar Mais",
|
||||
"noImagesInGallery": "Sem Imagens na Galeria",
|
||||
"generations": "Gerações",
|
||||
"showGenerations": "Mostrar Gerações",
|
||||
"uploads": "Enviados",
|
||||
"showUploads": "Mostrar Enviados",
|
||||
"galleryImageSize": "Tamanho da Imagem"
|
||||
},
|
||||
"hotkeys": {
|
||||
"generalHotkeys": "Atalhos Gerais",
|
||||
"galleryHotkeys": "Atalhos da Galeria",
|
||||
"toggleViewer": {
|
||||
"title": "Ativar Visualizador",
|
||||
"desc": "Abrir e fechar o Visualizador de Imagens"
|
||||
},
|
||||
"maximizeWorkSpace": {
|
||||
"desc": "Fechar painéis e maximixar área de trabalho",
|
||||
"title": "Maximizar a Área de Trabalho"
|
||||
},
|
||||
"changeTabs": {
|
||||
"title": "Mudar Guias",
|
||||
"desc": "Trocar para outra área de trabalho"
|
||||
},
|
||||
"consoleToggle": {
|
||||
"desc": "Abrir e fechar console",
|
||||
"title": "Ativar Console"
|
||||
},
|
||||
"setPrompt": {
|
||||
"title": "Definir Prompt",
|
||||
"desc": "Usar o prompt da imagem atual"
|
||||
},
|
||||
"sendToImageToImage": {
|
||||
"desc": "Manda a imagem atual para Imagem Para Imagem",
|
||||
"title": "Mandar para Imagem Para Imagem"
|
||||
},
|
||||
"previousImage": {
|
||||
"desc": "Mostra a imagem anterior na galeria",
|
||||
"title": "Imagem Anterior"
|
||||
},
|
||||
"nextImage": {
|
||||
"title": "Próxima Imagem",
|
||||
"desc": "Mostra a próxima imagem na galeria"
|
||||
},
|
||||
"decreaseGalleryThumbSize": {
|
||||
"desc": "Diminui o tamanho das thumbs na galeria",
|
||||
"title": "Diminuir Tamanho da Galeria de Imagem"
|
||||
},
|
||||
"selectBrush": {
|
||||
"title": "Selecionar Pincel",
|
||||
"desc": "Seleciona o pincel"
|
||||
},
|
||||
"selectEraser": {
|
||||
"title": "Selecionar Apagador",
|
||||
"desc": "Seleciona o apagador"
|
||||
},
|
||||
"decreaseBrushSize": {
|
||||
"title": "Diminuir Tamanho do Pincel",
|
||||
"desc": "Diminui o tamanho do pincel/apagador"
|
||||
},
|
||||
"increaseBrushOpacity": {
|
||||
"desc": "Aumenta a opacidade do pincel",
|
||||
"title": "Aumentar Opacidade do Pincel"
|
||||
},
|
||||
"moveTool": {
|
||||
"title": "Ferramenta Mover",
|
||||
"desc": "Permite navegar pela tela"
|
||||
},
|
||||
"decreaseBrushOpacity": {
|
||||
"desc": "Diminui a opacidade do pincel",
|
||||
"title": "Diminuir Opacidade do Pincel"
|
||||
},
|
||||
"toggleSnap": {
|
||||
"title": "Ativar Encaixe",
|
||||
"desc": "Ativa Encaixar na Grade"
|
||||
},
|
||||
"quickToggleMove": {
|
||||
"title": "Ativar Mover Rapidamente",
|
||||
"desc": "Temporariamente ativa o modo Mover"
|
||||
},
|
||||
"toggleLayer": {
|
||||
"title": "Ativar Camada",
|
||||
"desc": "Ativa a seleção de camada de máscara/base"
|
||||
},
|
||||
"clearMask": {
|
||||
"title": "Limpar Máscara",
|
||||
"desc": "Limpa toda a máscara"
|
||||
},
|
||||
"hideMask": {
|
||||
"title": "Esconder Máscara",
|
||||
"desc": "Esconde e Revela a máscara"
|
||||
},
|
||||
"mergeVisible": {
|
||||
"title": "Fundir Visível",
|
||||
"desc": "Fundir todas as camadas visíveis das telas"
|
||||
},
|
||||
"downloadImage": {
|
||||
"desc": "Descarregar a tela atual",
|
||||
"title": "Descarregar Imagem"
|
||||
},
|
||||
"undoStroke": {
|
||||
"title": "Desfazer Traço",
|
||||
"desc": "Desfaz um traço de pincel"
|
||||
},
|
||||
"redoStroke": {
|
||||
"title": "Refazer Traço",
|
||||
"desc": "Refaz o traço de pincel"
|
||||
},
|
||||
"keyboardShortcuts": "Atalhos de Teclado",
|
||||
"appHotkeys": "Atalhos do app",
|
||||
"invoke": {
|
||||
"title": "Invocar",
|
||||
"desc": "Gerar uma imagem"
|
||||
},
|
||||
"cancel": {
|
||||
"title": "Cancelar",
|
||||
"desc": "Cancelar geração de imagem"
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Foco do Prompt",
|
||||
"desc": "Foco da área de texto do prompt"
|
||||
},
|
||||
"toggleOptions": {
|
||||
"title": "Ativar Opções",
|
||||
"desc": "Abrir e fechar o painel de opções"
|
||||
},
|
||||
"pinOptions": {
|
||||
"title": "Fixar Opções",
|
||||
"desc": "Fixar o painel de opções"
|
||||
},
|
||||
"closePanels": {
|
||||
"title": "Fechar Painéis",
|
||||
"desc": "Fecha os painéis abertos"
|
||||
},
|
||||
"unifiedCanvasHotkeys": "Atalhos da Tela Unificada",
|
||||
"toggleGallery": {
|
||||
"title": "Ativar Galeria",
|
||||
"desc": "Abrir e fechar a gaveta da galeria"
|
||||
},
|
||||
"setSeed": {
|
||||
"title": "Definir Seed",
|
||||
"desc": "Usar seed da imagem atual"
|
||||
},
|
||||
"setParameters": {
|
||||
"title": "Definir Parâmetros",
|
||||
"desc": "Usar todos os parâmetros da imagem atual"
|
||||
},
|
||||
"restoreFaces": {
|
||||
"title": "Restaurar Rostos",
|
||||
"desc": "Restaurar a imagem atual"
|
||||
},
|
||||
"upscale": {
|
||||
"title": "Redimensionar",
|
||||
"desc": "Redimensionar a imagem atual"
|
||||
},
|
||||
"showInfo": {
|
||||
"title": "Mostrar Informações",
|
||||
"desc": "Mostrar metadados de informações da imagem atual"
|
||||
},
|
||||
"deleteImage": {
|
||||
"title": "Apagar Imagem",
|
||||
"desc": "Apaga a imagem atual"
|
||||
},
|
||||
"toggleGalleryPin": {
|
||||
"title": "Ativar Fixar Galeria",
|
||||
"desc": "Fixa e desafixa a galeria na interface"
|
||||
},
|
||||
"increaseGalleryThumbSize": {
|
||||
"title": "Aumentar Tamanho da Galeria de Imagem",
|
||||
"desc": "Aumenta o tamanho das thumbs na galeria"
|
||||
},
|
||||
"increaseBrushSize": {
|
||||
"title": "Aumentar Tamanho do Pincel",
|
||||
"desc": "Aumenta o tamanho do pincel/apagador"
|
||||
},
|
||||
"fillBoundingBox": {
|
||||
"title": "Preencher Caixa Delimitadora",
|
||||
"desc": "Preenche a caixa delimitadora com a cor do pincel"
|
||||
},
|
||||
"eraseBoundingBox": {
|
||||
"title": "Apagar Caixa Delimitadora",
|
||||
"desc": "Apaga a área da caixa delimitadora"
|
||||
},
|
||||
"colorPicker": {
|
||||
"title": "Selecionar Seletor de Cor",
|
||||
"desc": "Seleciona o seletor de cores"
|
||||
},
|
||||
"showHideBoundingBox": {
|
||||
"title": "Mostrar/Esconder Caixa Delimitadora",
|
||||
"desc": "Ativa a visibilidade da caixa delimitadora"
|
||||
},
|
||||
"saveToGallery": {
|
||||
"title": "Gravara Na Galeria",
|
||||
"desc": "Grava a tela atual na galeria"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"title": "Copiar para a Área de Transferência",
|
||||
"desc": "Copia a tela atual para a área de transferência"
|
||||
},
|
||||
"resetView": {
|
||||
"title": "Resetar Visualização",
|
||||
"desc": "Reseta Visualização da Tela"
|
||||
},
|
||||
"previousStagingImage": {
|
||||
"title": "Imagem de Preparação Anterior",
|
||||
"desc": "Área de Imagem de Preparação Anterior"
|
||||
},
|
||||
"nextStagingImage": {
|
||||
"title": "Próxima Imagem de Preparação Anterior",
|
||||
"desc": "Próxima Área de Imagem de Preparação Anterior"
|
||||
},
|
||||
"acceptStagingImage": {
|
||||
"title": "Aceitar Imagem de Preparação Anterior",
|
||||
"desc": "Aceitar Área de Imagem de Preparação Anterior"
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelAdded": "Modelo Adicionado",
|
||||
"modelUpdated": "Modelo Atualizado",
|
||||
"modelEntryDeleted": "Entrada de modelo excluída",
|
||||
"description": "Descrição",
|
||||
"modelLocationValidationMsg": "Caminho para onde o seu modelo está localizado.",
|
||||
"repo_id": "Repo ID",
|
||||
"vaeRepoIDValidationMsg": "Repositório Online do seu VAE",
|
||||
"width": "Largura",
|
||||
"widthValidationMsg": "Largura padrão do seu modelo.",
|
||||
"height": "Altura",
|
||||
"heightValidationMsg": "Altura padrão do seu modelo.",
|
||||
"findModels": "Encontrar Modelos",
|
||||
"scanAgain": "Digitalize Novamente",
|
||||
"deselectAll": "Deselecionar Tudo",
|
||||
"showExisting": "Mostrar Existente",
|
||||
"deleteConfig": "Apagar Config",
|
||||
"convertToDiffusersHelpText6": "Deseja converter este modelo?",
|
||||
"mergedModelName": "Nome do modelo mesclado",
|
||||
"alpha": "Alpha",
|
||||
"interpolationType": "Tipo de Interpolação",
|
||||
"modelMergeHeaderHelp1": "Pode mesclar até três modelos diferentes para criar uma mistura que atenda às suas necessidades.",
|
||||
"modelMergeHeaderHelp2": "Apenas Diffusers estão disponíveis para mesclagem. Se deseja mesclar um modelo de checkpoint, por favor, converta-o para Diffusers primeiro.",
|
||||
"modelMergeInterpAddDifferenceHelp": "Neste modo, o Modelo 3 é primeiro subtraído do Modelo 2. A versão resultante é mesclada com o Modelo 1 com a taxa alpha definida acima.",
|
||||
"nameValidationMsg": "Insira um nome para o seu modelo",
|
||||
"descriptionValidationMsg": "Adicione uma descrição para o seu modelo",
|
||||
"config": "Configuração",
|
||||
"modelExists": "Modelo Existe",
|
||||
"selectAndAdd": "Selecione e Adicione Modelos Listados Abaixo",
|
||||
"noModelsFound": "Nenhum Modelo Encontrado",
|
||||
"v2_768": "v2 (768px)",
|
||||
"inpainting": "v1 Inpainting",
|
||||
"customConfig": "Configuração personalizada",
|
||||
"pathToCustomConfig": "Caminho para configuração personalizada",
|
||||
"statusConverting": "A converter",
|
||||
"modelConverted": "Modelo Convertido",
|
||||
"ignoreMismatch": "Ignorar Divergências entre Modelos Selecionados",
|
||||
"addDifference": "Adicionar diferença",
|
||||
"pickModelType": "Escolha o tipo de modelo",
|
||||
"safetensorModels": "SafeTensors",
|
||||
"cannotUseSpaces": "Não pode usar espaços",
|
||||
"addNew": "Adicionar Novo",
|
||||
"addManually": "Adicionar Manualmente",
|
||||
"manual": "Manual",
|
||||
"name": "Nome",
|
||||
"configValidationMsg": "Caminho para o ficheiro de configuração do seu modelo.",
|
||||
"modelLocation": "Localização do modelo",
|
||||
"repoIDValidationMsg": "Repositório Online do seu Modelo",
|
||||
"updateModel": "Atualizar Modelo",
|
||||
"availableModels": "Modelos Disponíveis",
|
||||
"load": "Carregar",
|
||||
"active": "Ativado",
|
||||
"notLoaded": "Não carregado",
|
||||
"deleteModel": "Apagar modelo",
|
||||
"deleteMsg1": "Tem certeza de que deseja apagar esta entrada do modelo de InvokeAI?",
|
||||
"deleteMsg2": "Isso não vai apagar o ficheiro de modelo checkpoint do seu disco. Pode lê-los, se desejar.",
|
||||
"convertToDiffusers": "Converter para Diffusers",
|
||||
"convertToDiffusersHelpText1": "Este modelo será convertido ao formato 🧨 Diffusers.",
|
||||
"convertToDiffusersHelpText2": "Este processo irá substituir a sua entrada de Gestor de Modelos por uma versão Diffusers do mesmo modelo.",
|
||||
"convertToDiffusersHelpText3": "O seu ficheiro de ponto de verificação no disco NÃO será excluído ou modificado de forma alguma. Pode adicionar o seu ponto de verificação ao Gestor de modelos novamente, se desejar.",
|
||||
"convertToDiffusersSaveLocation": "Local para Gravar",
|
||||
"v2_base": "v2 (512px)",
|
||||
"mergeModels": "Mesclar modelos",
|
||||
"modelOne": "Modelo 1",
|
||||
"modelTwo": "Modelo 2",
|
||||
"modelThree": "Modelo 3",
|
||||
"mergedModelSaveLocation": "Local de Salvamento",
|
||||
"merge": "Mesclar",
|
||||
"modelsMerged": "Modelos mesclados",
|
||||
"mergedModelCustomSaveLocation": "Caminho Personalizado",
|
||||
"invokeAIFolder": "Pasta Invoke AI",
|
||||
"inverseSigmoid": "Sigmóide Inversa",
|
||||
"none": "nenhum",
|
||||
"modelManager": "Gerente de Modelo",
|
||||
"model": "Modelo",
|
||||
"allModels": "Todos os Modelos",
|
||||
"checkpointModels": "Checkpoints",
|
||||
"diffusersModels": "Diffusers",
|
||||
"addNewModel": "Adicionar Novo modelo",
|
||||
"addCheckpointModel": "Adicionar Modelo de Checkpoint/Safetensor",
|
||||
"addDiffuserModel": "Adicionar Diffusers",
|
||||
"vaeLocation": "Localização VAE",
|
||||
"vaeLocationValidationMsg": "Caminho para onde o seu VAE está localizado.",
|
||||
"vaeRepoID": "VAE Repo ID",
|
||||
"addModel": "Adicionar Modelo",
|
||||
"search": "Procurar",
|
||||
"cached": "Em cache",
|
||||
"checkpointFolder": "Pasta de Checkpoint",
|
||||
"clearCheckpointFolder": "Apagar Pasta de Checkpoint",
|
||||
"modelsFound": "Modelos Encontrados",
|
||||
"selectFolder": "Selecione a Pasta",
|
||||
"selected": "Selecionada",
|
||||
"selectAll": "Selecionar Tudo",
|
||||
"addSelected": "Adicione Selecionado",
|
||||
"delete": "Apagar",
|
||||
"formMessageDiffusersModelLocation": "Localização dos Modelos Diffusers",
|
||||
"formMessageDiffusersModelLocationDesc": "Por favor entre com ao menos um.",
|
||||
"formMessageDiffusersVAELocation": "Localização do VAE",
|
||||
"formMessageDiffusersVAELocationDesc": "Se não provido, InvokeAI irá procurar pelo ficheiro VAE dentro do local do modelo.",
|
||||
"convert": "Converter",
|
||||
"convertToDiffusersHelpText4": "Este é um processo único. Pode levar cerca de 30 a 60s, a depender das especificações do seu computador.",
|
||||
"convertToDiffusersHelpText5": "Por favor, certifique-se de que tenha espaço suficiente no disco. Os modelos geralmente variam entre 4GB e 7GB de tamanho.",
|
||||
"v1": "v1",
|
||||
"sameFolder": "Mesma pasta",
|
||||
"invokeRoot": "Pasta do InvokeAI",
|
||||
"custom": "Personalizado",
|
||||
"customSaveLocation": "Local de salvamento personalizado",
|
||||
"modelMergeAlphaHelp": "Alpha controla a força da mistura dos modelos. Valores de alpha mais baixos resultam numa influência menor do segundo modelo.",
|
||||
"sigmoid": "Sigmóide",
|
||||
"weightedSum": "Soma Ponderada"
|
||||
},
|
||||
"parameters": {
|
||||
"width": "Largura",
|
||||
"seed": "Seed",
|
||||
"hiresStrength": "Força da Alta Resolução",
|
||||
"negativePrompts": "Indicações negativas",
|
||||
"general": "Geral",
|
||||
"randomizeSeed": "Seed Aleatório",
|
||||
"shuffle": "Embaralhar",
|
||||
"noiseThreshold": "Limite de Ruído",
|
||||
"perlinNoise": "Ruído de Perlin",
|
||||
"variations": "Variatções",
|
||||
"seedWeights": "Pesos da Seed",
|
||||
"restoreFaces": "Restaurar Rostos",
|
||||
"faceRestoration": "Restauração de Rosto",
|
||||
"type": "Tipo",
|
||||
"denoisingStrength": "A força de remoção de ruído",
|
||||
"scale": "Escala",
|
||||
"otherOptions": "Outras Opções",
|
||||
"seamlessTiling": "Ladrilho Sem Fronteira",
|
||||
"hiresOptim": "Otimização de Alta Res",
|
||||
"imageFit": "Caber Imagem Inicial No Tamanho de Saída",
|
||||
"codeformerFidelity": "Fidelidade",
|
||||
"seamSize": "Tamanho da Fronteira",
|
||||
"seamBlur": "Desfoque da Fronteira",
|
||||
"seamStrength": "Força da Fronteira",
|
||||
"seamSteps": "Passos da Fronteira",
|
||||
"tileSize": "Tamanho do Ladrilho",
|
||||
"boundingBoxHeader": "Caixa Delimitadora",
|
||||
"seamCorrectionHeader": "Correção de Fronteira",
|
||||
"infillScalingHeader": "Preencimento e Escala",
|
||||
"img2imgStrength": "Força de Imagem Para Imagem",
|
||||
"toggleLoopback": "Ativar Loopback",
|
||||
"symmetry": "Simetria",
|
||||
"promptPlaceholder": "Digite o prompt aqui. [tokens negativos], (upweight)++, (downweight)--, trocar e misturar estão disponíveis (veja docs)",
|
||||
"sendTo": "Mandar para",
|
||||
"openInViewer": "Abrir No Visualizador",
|
||||
"closeViewer": "Fechar Visualizador",
|
||||
"usePrompt": "Usar Prompt",
|
||||
"deleteImage": "Apagar Imagem",
|
||||
"initialImage": "Imagem inicial",
|
||||
"showOptionsPanel": "Mostrar Painel de Opções",
|
||||
"strength": "Força",
|
||||
"upscaling": "Redimensionando",
|
||||
"upscale": "Redimensionar",
|
||||
"upscaleImage": "Redimensionar Imagem",
|
||||
"scaleBeforeProcessing": "Escala Antes do Processamento",
|
||||
"invoke": "Invocar",
|
||||
"images": "Imagems",
|
||||
"steps": "Passos",
|
||||
"cfgScale": "Escala CFG",
|
||||
"height": "Altura",
|
||||
"sampler": "Amostrador",
|
||||
"imageToImage": "Imagem para Imagem",
|
||||
"variationAmount": "Quntidade de Variatções",
|
||||
"scaledWidth": "L Escalada",
|
||||
"scaledHeight": "A Escalada",
|
||||
"infillMethod": "Método de Preenchimento",
|
||||
"hSymmetryStep": "H Passo de Simetria",
|
||||
"vSymmetryStep": "V Passo de Simetria",
|
||||
"cancel": {
|
||||
"immediate": "Cancelar imediatamente",
|
||||
"schedule": "Cancelar após a iteração atual",
|
||||
"isScheduled": "A cancelar",
|
||||
"setType": "Definir tipo de cancelamento"
|
||||
},
|
||||
"sendToImg2Img": "Mandar para Imagem Para Imagem",
|
||||
"sendToUnifiedCanvas": "Mandar para Tela Unificada",
|
||||
"copyImage": "Copiar imagem",
|
||||
"copyImageToLink": "Copiar Imagem Para a Ligação",
|
||||
"downloadImage": "Descarregar Imagem",
|
||||
"useSeed": "Usar Seed",
|
||||
"useAll": "Usar Todos",
|
||||
"useInitImg": "Usar Imagem Inicial",
|
||||
"info": "Informações"
|
||||
},
|
||||
"settings": {
|
||||
"confirmOnDelete": "Confirmar Antes de Apagar",
|
||||
"displayHelpIcons": "Mostrar Ícones de Ajuda",
|
||||
"useCanvasBeta": "Usar Layout de Telas Beta",
|
||||
"enableImageDebugging": "Ativar Depuração de Imagem",
|
||||
"useSlidersForAll": "Usar deslizadores para todas as opções",
|
||||
"resetWebUIDesc1": "Reiniciar a interface apenas reinicia o cache local do broswer para imagens e configurações lembradas. Não apaga nenhuma imagem do disco.",
|
||||
"models": "Modelos",
|
||||
"displayInProgress": "Mostrar Progresso de Imagens Em Andamento",
|
||||
"saveSteps": "Gravar imagens a cada n passos",
|
||||
"resetWebUI": "Reiniciar Interface",
|
||||
"resetWebUIDesc2": "Se as imagens não estão a aparecer na galeria ou algo mais não está a funcionar, favor tentar reiniciar antes de postar um problema no GitHub.",
|
||||
"resetComplete": "A interface foi reiniciada. Atualize a página para carregar."
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Envio Falhou",
|
||||
"uploadFailedMultipleImagesDesc": "Várias imagens copiadas, só é permitido uma imagem de cada vez",
|
||||
"uploadFailedUnableToLoadDesc": "Não foj possível carregar o ficheiro",
|
||||
"downloadImageStarted": "Download de Imagem Começou",
|
||||
"imageNotLoadedDesc": "Nenhuma imagem encontrada a enviar para o módulo de imagem para imagem",
|
||||
"imageLinkCopied": "Ligação de Imagem Copiada",
|
||||
"imageNotLoaded": "Nenhuma Imagem Carregada",
|
||||
"parametersFailed": "Problema ao carregar parâmetros",
|
||||
"parametersFailedDesc": "Não foi possível carregar imagem incial.",
|
||||
"seedSet": "Seed Definida",
|
||||
"upscalingFailed": "Redimensionamento Falhou",
|
||||
"promptNotSet": "Prompt Não Definido",
|
||||
"tempFoldersEmptied": "Pasta de Ficheiros Temporários Esvaziada",
|
||||
"imageCopied": "Imagem Copiada",
|
||||
"imageSavedToGallery": "Imagem Salva na Galeria",
|
||||
"canvasMerged": "Tela Fundida",
|
||||
"sentToImageToImage": "Mandar Para Imagem Para Imagem",
|
||||
"sentToUnifiedCanvas": "Enviada para a Tela Unificada",
|
||||
"parametersSet": "Parâmetros Definidos",
|
||||
"parametersNotSet": "Parâmetros Não Definidos",
|
||||
"parametersNotSetDesc": "Nenhum metadado foi encontrado para essa imagem.",
|
||||
"seedNotSet": "Seed Não Definida",
|
||||
"seedNotSetDesc": "Não foi possível achar a seed para a imagem.",
|
||||
"promptSet": "Prompt Definido",
|
||||
"promptNotSetDesc": "Não foi possível achar prompt para essa imagem.",
|
||||
"faceRestoreFailed": "Restauração de Rosto Falhou",
|
||||
"metadataLoadFailed": "Falha ao tentar carregar metadados",
|
||||
"initialImageSet": "Imagem Inicial Definida",
|
||||
"initialImageNotSet": "Imagem Inicial Não Definida",
|
||||
"initialImageNotSetDesc": "Não foi possível carregar imagem incial"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
"prompt": "Este é o campo de prompt. O prompt inclui objetos de geração e termos estilísticos. Também pode adicionar peso (importância do token) no prompt, mas comandos e parâmetros de CLI não funcionarão.",
|
||||
"other": "Essas opções ativam modos alternativos de processamento para o Invoke. 'Seamless tiling' criará padrões repetidos na saída. 'High resolution' é uma geração em duas etapas com img2img: use essa configuração quando desejar uma imagem maior e mais coerente sem artefatos. Levará mais tempo do que o txt2img usual.",
|
||||
"seed": "O valor da semente afeta o ruído inicial a partir do qual a imagem é formada. Pode usar as sementes já existentes de imagens anteriores. 'Limiar de ruído' é usado para mitigar artefatos em valores CFG altos (experimente a faixa de 0-10) e o Perlin para adicionar ruído Perlin durante a geração: ambos servem para adicionar variação às suas saídas.",
|
||||
"imageToImage": "Image to Image carrega qualquer imagem como inicial, que é então usada para gerar uma nova junto com o prompt. Quanto maior o valor, mais a imagem resultante mudará. Valores de 0.0 a 1.0 são possíveis, a faixa recomendada é de 0.25 a 0.75",
|
||||
"faceCorrection": "Correção de rosto com GFPGAN ou Codeformer: o algoritmo detecta rostos na imagem e corrige quaisquer defeitos. Um valor alto mudará mais a imagem, a resultar em rostos mais atraentes. Codeformer com uma fidelidade maior preserva a imagem original às custas de uma correção de rosto mais forte.",
|
||||
"seamCorrection": "Controla o tratamento das emendas visíveis que ocorrem entre as imagens geradas no canvas.",
|
||||
"gallery": "A galeria exibe as gerações da pasta de saída conforme elas são criadas. As configurações são armazenadas em ficheiros e acessadas pelo menu de contexto.",
|
||||
"variations": "Experimente uma variação com um valor entre 0,1 e 1,0 para mudar o resultado para uma determinada semente. Variações interessantes da semente estão entre 0,1 e 0,3.",
|
||||
"upscale": "Use o ESRGAN para ampliar a imagem imediatamente após a geração.",
|
||||
"boundingBox": "A caixa delimitadora é a mesma que as configurações de largura e altura para Texto para Imagem ou Imagem para Imagem. Apenas a área na caixa será processada.",
|
||||
"infillAndScaling": "Gira os métodos de preenchimento (usados em áreas mascaradas ou apagadas do canvas) e a escala (útil para tamanhos de caixa delimitadora pequenos)."
|
||||
}
|
||||
},
|
||||
"unifiedCanvas": {
|
||||
"emptyTempImagesFolderMessage": "Esvaziar a pasta de ficheiros de imagem temporários também reseta completamente a Tela Unificada. Isso inclui todo o histórico de desfazer/refazer, imagens na área de preparação e a camada base da tela.",
|
||||
"scaledBoundingBox": "Caixa Delimitadora Escalada",
|
||||
"boundingBoxPosition": "Posição da Caixa Delimitadora",
|
||||
"next": "Próximo",
|
||||
"accept": "Aceitar",
|
||||
"showHide": "Mostrar/Esconder",
|
||||
"discardAll": "Descartar Todos",
|
||||
"betaClear": "Limpar",
|
||||
"betaDarkenOutside": "Escurecer Externamente",
|
||||
"base": "Base",
|
||||
"brush": "Pincel",
|
||||
"showIntermediates": "Mostrar Intermediários",
|
||||
"showGrid": "Mostrar Grade",
|
||||
"clearCanvasHistoryConfirm": "Tem certeza que quer limpar o histórico de tela?",
|
||||
"boundingBox": "Caixa Delimitadora",
|
||||
"canvasDimensions": "Dimensões da Tela",
|
||||
"canvasPosition": "Posição da Tela",
|
||||
"cursorPosition": "Posição do cursor",
|
||||
"previous": "Anterior",
|
||||
"betaLimitToBox": "Limitar á Caixa",
|
||||
"layer": "Camada",
|
||||
"mask": "Máscara",
|
||||
"maskingOptions": "Opções de Mascaramento",
|
||||
"enableMask": "Ativar Máscara",
|
||||
"preserveMaskedArea": "Preservar Área da Máscara",
|
||||
"clearMask": "Limpar Máscara",
|
||||
"eraser": "Apagador",
|
||||
"fillBoundingBox": "Preencher Caixa Delimitadora",
|
||||
"eraseBoundingBox": "Apagar Caixa Delimitadora",
|
||||
"colorPicker": "Seletor de Cor",
|
||||
"brushOptions": "Opções de Pincel",
|
||||
"brushSize": "Tamanho",
|
||||
"move": "Mover",
|
||||
"resetView": "Resetar Visualização",
|
||||
"mergeVisible": "Fundir Visível",
|
||||
"saveToGallery": "Gravar na Galeria",
|
||||
"copyToClipboard": "Copiar para a Área de Transferência",
|
||||
"downloadAsImage": "Descarregar Como Imagem",
|
||||
"undo": "Desfazer",
|
||||
"redo": "Refazer",
|
||||
"clearCanvas": "Limpar Tela",
|
||||
"canvasSettings": "Configurações de Tela",
|
||||
"snapToGrid": "Encaixar na Grade",
|
||||
"darkenOutsideSelection": "Escurecer Seleção Externa",
|
||||
"autoSaveToGallery": "Gravar Automaticamente na Galeria",
|
||||
"saveBoxRegionOnly": "Gravar Apenas a Região da Caixa",
|
||||
"limitStrokesToBox": "Limitar Traços à Caixa",
|
||||
"showCanvasDebugInfo": "Mostrar Informações de Depuração daTela",
|
||||
"clearCanvasHistory": "Limpar o Histórico da Tela",
|
||||
"clearHistory": "Limpar Históprico",
|
||||
"clearCanvasHistoryMessage": "Limpar o histórico de tela deixa a sua tela atual intacta, mas limpa de forma irreversível o histórico de desfazer e refazer.",
|
||||
"emptyTempImageFolder": "Esvaziar a Pasta de Ficheiros de Imagem Temporários",
|
||||
"emptyFolder": "Esvaziar Pasta",
|
||||
"emptyTempImagesFolderConfirm": "Tem certeza que quer esvaziar a pasta de ficheiros de imagem temporários?",
|
||||
"activeLayer": "Camada Ativa",
|
||||
"canvasScale": "Escala da Tela",
|
||||
"betaPreserveMasked": "Preservar Máscarado"
|
||||
},
|
||||
"accessibility": {
|
||||
"invokeProgressBar": "Invocar barra de progresso",
|
||||
"reset": "Repôr",
|
||||
"nextImage": "Próxima imagem",
|
||||
"useThisParameter": "Usar este parâmetro",
|
||||
"copyMetadataJson": "Copiar metadados JSON",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reduzir",
|
||||
"rotateCounterClockwise": "Girar no sentido anti-horário",
|
||||
"rotateClockwise": "Girar no sentido horário",
|
||||
"flipVertically": "Espelhar verticalmente",
|
||||
"modifyConfig": "Modificar config",
|
||||
"toggleAutoscroll": "Alternar rolagem automática",
|
||||
"showGallery": "Mostrar galeria",
|
||||
"showOptionsPanel": "Mostrar painel de opções",
|
||||
"uploadImage": "Enviar imagem",
|
||||
"previousImage": "Imagem anterior",
|
||||
"flipHorizontally": "Espelhar horizontalmente",
|
||||
"toggleLogViewer": "Alternar visualizador de registo"
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,10 @@
|
||||
"statusMergingModels": "Mesclando Modelos",
|
||||
"statusMergedModels": "Modelos Mesclados",
|
||||
"langRussian": "Russo",
|
||||
"langSpanish": "Espanhol"
|
||||
"langSpanish": "Espanhol",
|
||||
"pinOptionsPanel": "Fixar painel de opções",
|
||||
"loadingInvokeAI": "Carregando Invoke AI",
|
||||
"loading": "Carregando"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Gerações",
|
||||
|
38
invokeai/frontend/web/dist/locales/ru.json
vendored
38
invokeai/frontend/web/dist/locales/ru.json
vendored
@ -46,7 +46,15 @@
|
||||
"statusLoadingModel": "Загрузка модели",
|
||||
"statusModelChanged": "Модель изменена",
|
||||
"githubLabel": "Github",
|
||||
"discordLabel": "Discord"
|
||||
"discordLabel": "Discord",
|
||||
"statusMergingModels": "Слияние моделей",
|
||||
"statusModelConverted": "Модель сконвертирована",
|
||||
"statusMergedModels": "Модели объединены",
|
||||
"pinOptionsPanel": "Закрепить панель настроек",
|
||||
"loading": "Загрузка",
|
||||
"loadingInvokeAI": "Загрузка Invoke AI",
|
||||
"back": "Назад",
|
||||
"statusConvertingModel": "Конвертация модели"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Генерации",
|
||||
@ -323,7 +331,30 @@
|
||||
"deleteConfig": "Удалить конфигурацию",
|
||||
"deleteMsg1": "Вы точно хотите удалить модель из InvokeAI?",
|
||||
"deleteMsg2": "Это не удалит файл модели с диска. Позже вы можете добавить его снова.",
|
||||
"repoIDValidationMsg": "Онлайн-репозиторий модели"
|
||||
"repoIDValidationMsg": "Онлайн-репозиторий модели",
|
||||
"convertToDiffusersHelpText5": "Пожалуйста, убедитесь, что у вас достаточно места на диске. Модели обычно занимают 4 – 7 Гб.",
|
||||
"invokeAIFolder": "Каталог InvokeAI",
|
||||
"ignoreMismatch": "Игнорировать несоответствия между выбранными моделями",
|
||||
"addCheckpointModel": "Добавить модель Checkpoint/Safetensor",
|
||||
"formMessageDiffusersModelLocationDesc": "Укажите хотя бы одно.",
|
||||
"convertToDiffusersHelpText3": "Файл модели на диске НЕ будет удалён или изменён. Вы сможете заново добавить его в Model Manager при необходимости.",
|
||||
"vaeRepoID": "ID репозитория VAE",
|
||||
"mergedModelName": "Название объединенной модели",
|
||||
"checkpointModels": "Checkpoints",
|
||||
"allModels": "Все модели",
|
||||
"addDiffuserModel": "Добавить Diffusers",
|
||||
"repo_id": "ID репозитория",
|
||||
"formMessageDiffusersVAELocationDesc": "Если не указано, InvokeAI будет искать файл VAE рядом с моделью.",
|
||||
"convert": "Преобразовать",
|
||||
"convertToDiffusers": "Преобразовать в Diffusers",
|
||||
"convertToDiffusersHelpText1": "Модель будет преобразована в формат 🧨 Diffusers.",
|
||||
"convertToDiffusersHelpText4": "Это единоразовое действие. Оно может занять 30—60 секунд в зависимости от характеристик вашего компьютера.",
|
||||
"convertToDiffusersHelpText6": "Вы хотите преобразовать эту модель?",
|
||||
"statusConverting": "Преобразование",
|
||||
"modelConverted": "Модель преобразована",
|
||||
"invokeRoot": "Каталог InvokeAI",
|
||||
"modelsMerged": "Модели объединены",
|
||||
"mergeModels": "Объединить модели"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Изображения",
|
||||
@ -503,5 +534,8 @@
|
||||
"betaDarkenOutside": "Затемнить снаружи",
|
||||
"betaLimitToBox": "Ограничить выделением",
|
||||
"betaPreserveMasked": "Сохранять маскируемую область"
|
||||
},
|
||||
"accessibility": {
|
||||
"modelSelect": "Выбор модели"
|
||||
}
|
||||
}
|
||||
|
17
invokeai/frontend/web/dist/locales/zh_Hant.json
vendored
17
invokeai/frontend/web/dist/locales/zh_Hant.json
vendored
@ -19,6 +19,21 @@
|
||||
"discordLabel": "Discord",
|
||||
"nodesDesc": "使用Node生成圖像的系統正在開發中。敬請期待有關於這項功能的更新。",
|
||||
"reportBugLabel": "回報錯誤",
|
||||
"githubLabel": "GitHub"
|
||||
"githubLabel": "GitHub",
|
||||
"langKorean": "韓語",
|
||||
"langPortuguese": "葡萄牙語",
|
||||
"hotkeysLabel": "快捷鍵",
|
||||
"languagePickerLabel": "切換語言",
|
||||
"langDutch": "荷蘭語",
|
||||
"langFrench": "法語",
|
||||
"langGerman": "德語",
|
||||
"langItalian": "義大利語",
|
||||
"langJapanese": "日語",
|
||||
"langPolish": "波蘭語",
|
||||
"langBrPortuguese": "巴西葡萄牙語",
|
||||
"langRussian": "俄語",
|
||||
"langSpanish": "西班牙語",
|
||||
"text2img": "文字到圖像",
|
||||
"unifiedCanvas": "統一畫布"
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"build": "yarn run lint && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint:madge": "madge --circular src/main.tsx",
|
||||
"lint:eslint": "eslint --max-warnings=0",
|
||||
"lint:eslint": "eslint --max-warnings=0 .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:tsc && yarn run lint:madge",
|
||||
|
@ -49,10 +49,11 @@
|
||||
"langSimplifiedChinese": "简体中文",
|
||||
"langUkranian": "Украї́нська",
|
||||
"langSpanish": "Español",
|
||||
"text2img": "Text To Image",
|
||||
"txt2img": "Text To Image",
|
||||
"img2img": "Image To Image",
|
||||
"unifiedCanvas": "Unified Canvas",
|
||||
"nodes": "Nodes",
|
||||
"postprocessing": "Post Processing",
|
||||
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
|
||||
"postProcessing": "Post Processing",
|
||||
"postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.",
|
||||
@ -596,7 +597,7 @@
|
||||
"autoSaveToGallery": "Auto Save to Gallery",
|
||||
"saveBoxRegionOnly": "Save Box Region Only",
|
||||
"limitStrokesToBox": "Limit Strokes to Box",
|
||||
"showCanvasDebugInfo": "Show Canvas Debug Info",
|
||||
"showCanvasDebugInfo": "Show Additional Canvas Info",
|
||||
"clearCanvasHistory": "Clear Canvas History",
|
||||
"clearHistory": "Clear History",
|
||||
"clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.",
|
||||
|
@ -9,34 +9,53 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
||||
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||
import { Box, Grid } from '@chakra-ui/react';
|
||||
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants';
|
||||
import { Box, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react';
|
||||
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
||||
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
|
||||
import Lightbox from 'features/lightbox/components/Lightbox';
|
||||
import { useAppSelector } from './storeHooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
keepGUIAlive();
|
||||
|
||||
const App = () => {
|
||||
useToastWatcher();
|
||||
|
||||
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
||||
const { setColorMode } = useColorMode();
|
||||
|
||||
useEffect(() => {
|
||||
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
|
||||
}, [setColorMode, currentTheme]);
|
||||
|
||||
return (
|
||||
<Grid w="100vw" h="100vh">
|
||||
<Lightbox />
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
p={APP_PADDING}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
<Flex gap={4} w="full" h="full">
|
||||
<InvokeTabs />
|
||||
<ImageGalleryPanel />
|
||||
</Flex>
|
||||
</Grid>
|
||||
<Box>
|
||||
<Console />
|
||||
</Box>
|
||||
</ImageUploader>
|
||||
<FloatingParametersPanelButtons />
|
||||
<FloatingGalleryButton />
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
@ -9,6 +9,15 @@ import { greenTeaThemeColors } from 'theme/colors/greenTea';
|
||||
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
|
||||
import { lightThemeColors } from 'theme/colors/lightTheme';
|
||||
import { oceanBlueColors } from 'theme/colors/oceanBlue';
|
||||
import '@fontsource/inter/100.css';
|
||||
import '@fontsource/inter/200.css';
|
||||
import '@fontsource/inter/300.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import '@fontsource/inter/800.css';
|
||||
import '@fontsource/inter/900.css';
|
||||
|
||||
type ThemeLocaleProviderProps = {
|
||||
children: ReactNode;
|
||||
|
@ -57,10 +57,13 @@ const galleryBlacklist = [
|
||||
'currentImage',
|
||||
'currentImageUuid',
|
||||
'shouldAutoSwitchToNewImages',
|
||||
'shouldHoldGalleryOpen',
|
||||
'intermediateImage',
|
||||
].map((blacklistItem) => `gallery.${blacklistItem}`);
|
||||
|
||||
const lightboxBlacklist = ['isLightboxOpen'].map(
|
||||
(blacklistItem) => `lightbox.${blacklistItem}`
|
||||
);
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
generation: generationReducer,
|
||||
postprocessing: postprocessingReducer,
|
||||
@ -75,7 +78,12 @@ const rootPersistConfig = getPersistConfig({
|
||||
key: 'root',
|
||||
storage,
|
||||
rootReducer,
|
||||
blacklist: [...canvasBlacklist, ...systemBlacklist, ...galleryBlacklist],
|
||||
blacklist: [
|
||||
...canvasBlacklist,
|
||||
...systemBlacklist,
|
||||
...galleryBlacklist,
|
||||
...lightboxBlacklist,
|
||||
],
|
||||
debounce: 300,
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Box, forwardRef, Icon } from '@chakra-ui/react';
|
||||
import { Feature } from 'app/features';
|
||||
import { memo } from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { MdHelp } from 'react-icons/md';
|
||||
import GuidePopover from './GuidePopover';
|
||||
@ -19,4 +20,4 @@ const GuideIcon = forwardRef(
|
||||
)
|
||||
);
|
||||
|
||||
export default GuideIcon;
|
||||
export default memo(GuideIcon);
|
||||
|
@ -11,7 +11,7 @@ import { Feature, useFeatureHelpInfo } from 'app/features';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import { ReactElement } from 'react';
|
||||
import { memo, ReactElement } from 'react';
|
||||
|
||||
type GuideProps = {
|
||||
children: ReactElement;
|
||||
@ -30,7 +30,7 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
if (!shouldDisplayGuides) return null;
|
||||
|
||||
return (
|
||||
<Popover trigger="hover">
|
||||
<Popover trigger="hover" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
</PopoverTrigger>
|
||||
@ -46,4 +46,4 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GuidePopover;
|
||||
export default memo(GuidePopover);
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
forwardRef,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { cloneElement, ReactElement, ReactNode, useRef } from 'react';
|
||||
import { cloneElement, memo, ReactElement, ReactNode, useRef } from 'react';
|
||||
import IAIButton from './IAIButton';
|
||||
|
||||
type Props = {
|
||||
@ -79,4 +79,4 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default IAIAlertDialog;
|
||||
export default memo(IAIAlertDialog);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
export interface IAIButtonProps extends ButtonProps {
|
||||
tooltip?: string;
|
||||
@ -25,4 +25,4 @@ const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default IAIButton;
|
||||
export default memo(IAIButton);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
type IAICheckboxProps = CheckboxProps & {
|
||||
label: string | ReactNode;
|
||||
@ -14,4 +14,4 @@ const IAICheckbox = (props: IAICheckboxProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICheckbox;
|
||||
export default memo(IAICheckbox);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { chakra, ChakraProps } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { RgbaColorPicker } from 'react-colorful';
|
||||
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
|
||||
|
||||
@ -35,4 +36,4 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIColorPicker;
|
||||
export default memo(IAIColorPicker);
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { FormErrorMessage, FormErrorMessageProps } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIFormErrorMessageProps = FormErrorMessageProps & {
|
||||
children: ReactNode | string;
|
||||
};
|
||||
|
||||
export default function IAIFormErrorMessage(props: IAIFormErrorMessageProps) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<FormErrorMessage color="error.400" {...rest}>
|
||||
{children}
|
||||
</FormErrorMessage>
|
||||
);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { FormHelperText, FormHelperTextProps } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIFormHelperTextProps = FormHelperTextProps & {
|
||||
children: ReactNode | string;
|
||||
};
|
||||
|
||||
export default function IAIFormHelperText(props: IAIFormHelperTextProps) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<FormHelperText margin={0} color="base.400" {...rest}>
|
||||
{children}
|
||||
</FormHelperText>
|
||||
);
|
||||
}
|
@ -5,6 +5,7 @@ import {
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type IAIIconButtonProps = IconButtonProps & {
|
||||
tooltip?: string;
|
||||
@ -33,4 +34,4 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default IAIIconButton;
|
||||
export default memo(IAIIconButton);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Input,
|
||||
InputProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { ChangeEvent, memo } from 'react';
|
||||
|
||||
interface IAIInputProps extends InputProps {
|
||||
label?: string;
|
||||
@ -15,7 +15,7 @@ interface IAIInputProps extends InputProps {
|
||||
formControlProps?: Omit<FormControlProps, 'isInvalid' | 'isDisabled'>;
|
||||
}
|
||||
|
||||
export default function IAIInput(props: IAIInputProps) {
|
||||
const IAIInput = (props: IAIInputProps) => {
|
||||
const {
|
||||
label = '',
|
||||
isDisabled = false,
|
||||
@ -34,4 +34,6 @@ export default function IAIInput(props: IAIInputProps) {
|
||||
<Input {...rest} />
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(IAIInput);
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { clamp } from 'lodash';
|
||||
|
||||
import { FocusEvent, useEffect, useState } from 'react';
|
||||
import { FocusEvent, memo, useEffect, useState } from 'react';
|
||||
|
||||
const numberStringRegex = /^-?(0\.)?\.?$/;
|
||||
|
||||
@ -139,4 +139,4 @@ const IAINumberInput = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAINumberInput;
|
||||
export default memo(IAINumberInput);
|
||||
|
18
invokeai/frontend/web/src/common/components/IAIOption.tsx
Normal file
18
invokeai/frontend/web/src/common/components/IAIOption.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIOptionProps = {
|
||||
children: ReactNode | string | number;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
export default function IAIOption(props: IAIOptionProps) {
|
||||
const { children, value } = props;
|
||||
const [base800, base200] = useToken('colors', ['base.800', 'base.200']);
|
||||
|
||||
return (
|
||||
<option value={value} style={{ background: base800, color: base200 }}>
|
||||
{children}
|
||||
</option>
|
||||
);
|
||||
}
|
@ -6,7 +6,7 @@ import {
|
||||
PopoverProps,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
type IAIPopoverProps = PopoverProps & {
|
||||
triggerComponent: ReactNode;
|
||||
@ -35,4 +35,4 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIPopover;
|
||||
export default memo(IAIPopover);
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { MouseEvent } from 'react';
|
||||
import { memo, MouseEvent } from 'react';
|
||||
import IAIOption from './IAIOption';
|
||||
|
||||
type IAISelectProps = SelectProps & {
|
||||
label?: string;
|
||||
@ -37,13 +38,13 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
<Select {...rest}>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt}>
|
||||
<IAIOption key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
</IAIOption>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
<IAIOption key={opt.value} value={opt.value}>
|
||||
{opt.key}
|
||||
</option>
|
||||
</IAIOption>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
@ -52,4 +53,4 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAISelect;
|
||||
export default memo(IAISelect);
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
IconButtonProps,
|
||||
ButtonProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { MouseEventHandler, ReactNode } from 'react';
|
||||
import { memo, MouseEventHandler, ReactNode } from 'react';
|
||||
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
|
||||
|
||||
interface IAIMenuItem {
|
||||
@ -31,7 +31,7 @@ interface IAIMenuProps {
|
||||
menuItemProps?: MenuItemProps;
|
||||
}
|
||||
|
||||
export default function IAISimpleMenu(props: IAIMenuProps) {
|
||||
const IAISimpleMenu = (props: IAIMenuProps) => {
|
||||
const {
|
||||
menuType = 'icon',
|
||||
iconTooltip,
|
||||
@ -83,4 +83,6 @@ export default function IAISimpleMenu(props: IAIMenuProps) {
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(IAISimpleMenu);
|
||||
|
@ -25,8 +25,8 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { clamp } from 'lodash';
|
||||
|
||||
import { FocusEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
|
||||
|
||||
@ -62,7 +62,7 @@ export type IAIFullSliderProps = {
|
||||
sliderIAIIconButtonProps?: IAIIconButtonProps;
|
||||
};
|
||||
|
||||
export default function IAISlider(props: IAIFullSliderProps) {
|
||||
const IAISlider = (props: IAIFullSliderProps) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const {
|
||||
label,
|
||||
@ -174,16 +174,22 @@ export default function IAISlider(props: IAIFullSliderProps) {
|
||||
<>
|
||||
<SliderMark
|
||||
value={min}
|
||||
insetInlineStart={0}
|
||||
sx={{ insetInlineStart: 'unset !important' }}
|
||||
// insetInlineStart={0}
|
||||
sx={{
|
||||
insetInlineStart: '0 !important',
|
||||
insetInlineEnd: 'unset !important',
|
||||
}}
|
||||
{...sliderMarkProps}
|
||||
>
|
||||
{min}
|
||||
</SliderMark>
|
||||
<SliderMark
|
||||
value={max}
|
||||
insetInlineEnd={0}
|
||||
sx={{ insetInlineStart: 'unset !important' }}
|
||||
// insetInlineEnd={0}
|
||||
sx={{
|
||||
insetInlineStart: 'unset !important',
|
||||
insetInlineEnd: '0 !important',
|
||||
}}
|
||||
{...sliderMarkProps}
|
||||
>
|
||||
{max}
|
||||
@ -248,4 +254,6 @@ export default function IAISlider(props: IAIFullSliderProps) {
|
||||
</HStack>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(IAISlider);
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
Switch,
|
||||
SwitchProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface Props extends SwitchProps {
|
||||
label?: string;
|
||||
@ -44,4 +45,4 @@ const IAISwitch = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default IAISwitch;
|
||||
export default memo(IAISwitch);
|
||||
|
@ -3,10 +3,11 @@ import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerCo
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
|
||||
import { tabDict } from 'features/ui/components/InvokeTabs';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { ResourceKey } from 'i18next';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
memo,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -134,7 +135,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
|
||||
activeTabName
|
||||
)
|
||||
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
||||
? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`
|
||||
: ``;
|
||||
|
||||
return (
|
||||
@ -161,4 +162,4 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
||||
export default memo(ImageUploader);
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
|
||||
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
@ -22,13 +23,6 @@ const selector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const gridLinesColor = {
|
||||
dark: 'rgba(255, 255, 255, 0.2)',
|
||||
green: 'rgba(255, 255, 255, 0.2)',
|
||||
light: 'rgba(0, 0, 0, 0.2)',
|
||||
ocean: 'rgba(136, 148, 184, 0.2)',
|
||||
};
|
||||
|
||||
const IAICanvasGrid = () => {
|
||||
const currentTheme = useAppSelector(
|
||||
(state: RootState) => state.ui.currentTheme
|
||||
@ -37,6 +31,8 @@ const IAICanvasGrid = () => {
|
||||
useAppSelector(selector);
|
||||
const [gridLines, setGridLines] = useState<ReactNode[]>([]);
|
||||
|
||||
const [gridLineColor] = useToken('colors', ['gridLineColor']);
|
||||
|
||||
const unscale = useCallback(
|
||||
(value: number) => {
|
||||
return value / stageScale;
|
||||
@ -45,9 +41,6 @@ const IAICanvasGrid = () => {
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const gridLineColor =
|
||||
gridLinesColor[currentTheme as keyof typeof gridLinesColor];
|
||||
|
||||
const { width, height } = stageDimensions;
|
||||
const { x, y } = stageCoordinates;
|
||||
|
||||
@ -112,7 +105,14 @@ const IAICanvasGrid = () => {
|
||||
));
|
||||
|
||||
setGridLines(xLines.concat(yLines));
|
||||
}, [stageScale, stageCoordinates, stageDimensions, currentTheme, unscale]);
|
||||
}, [
|
||||
stageScale,
|
||||
stageCoordinates,
|
||||
stageDimensions,
|
||||
currentTheme,
|
||||
unscale,
|
||||
gridLineColor,
|
||||
]);
|
||||
|
||||
return <Group>{gridLines}</Group>;
|
||||
};
|
||||
|
@ -104,7 +104,7 @@ const IAICanvasStatusText = () => {
|
||||
margin: 1,
|
||||
borderRadius: 'base',
|
||||
pointerEvents: 'none',
|
||||
bg: 'blackAlpha.500',
|
||||
bg: 'base.800',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { AppDispatch, AppGetState } from 'app/store';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { debounce } from 'lodash';
|
||||
import { setDoesCanvasNeedScaling } from '../canvasSlice';
|
||||
|
||||
const debouncedCanvasScale = debounce((dispatch: AppDispatch) => {
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
}, 300);
|
||||
|
||||
export const requestCanvasRescale =
|
||||
() => (dispatch: AppDispatch, getState: AppGetState) => {
|
||||
const activeTabName = activeTabNameSelector(getState());
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
debouncedCanvasScale(dispatch);
|
||||
}
|
||||
};
|
@ -7,10 +7,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import {
|
||||
setDoesCanvasNeedScaling,
|
||||
setInitialCanvasImage,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { GalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
||||
@ -52,6 +49,7 @@ import { gallerySelector } from '../store/gallerySelectors';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { useCallback } from 'react';
|
||||
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[
|
||||
@ -361,7 +359,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
|
||||
|
||||
dispatch(setInitialCanvasImage(currentImage));
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
dispatch(requestCanvasRescale());
|
||||
|
||||
if (activeTabName !== 'unifiedCanvas') {
|
||||
dispatch(setActiveTab('unifiedCanvas'));
|
||||
@ -419,7 +417,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
rowGap: 2,
|
||||
w: 52,
|
||||
}}
|
||||
>
|
||||
<IAIButton
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Flex, Image } from '@chakra-ui/react';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import { GalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
|
||||
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
@ -45,6 +46,8 @@ export default function CurrentImagePreview() {
|
||||
{imageToDisplay && (
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
width={imageToDisplay.width}
|
||||
height={imageToDisplay.height}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
maxWidth: '100%',
|
||||
@ -54,18 +57,23 @@ export default function CurrentImagePreview() {
|
||||
imageRendering: isIntermediate ? 'pixelated' : 'initial',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
{...(isIntermediate && {
|
||||
width: imageToDisplay.width,
|
||||
height: imageToDisplay.height,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
||||
{shouldShowImageDetails && imageToDisplay && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
styleClass="current-image-metadata"
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'base',
|
||||
overflow: 'scroll',
|
||||
maxHeight: APP_METADATA_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,254 @@
|
||||
import { ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react';
|
||||
import { requestImages } from 'app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { imageGallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
setCurrentCategory,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setShouldUseSingleGalleryColumn,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
||||
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
import Scrollable from 'features/ui/components/common/Scrollable';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
|
||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
|
||||
|
||||
const ImageGalleryContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
||||
|
||||
const {
|
||||
images,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryGridTemplateColumns,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
areMoreImagesAvailable,
|
||||
shouldUseSingleGalleryColumn,
|
||||
} = useAppSelector(imageGallerySelector);
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(requestImages(currentCategory));
|
||||
};
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
dispatch(setGalleryImageMinimumWidth(v));
|
||||
};
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(togglePinGalleryPanel());
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!resizeObserverRef.current) {
|
||||
return;
|
||||
}
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (!resizeObserverRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
resizeObserverRef.current.clientWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH
|
||||
) {
|
||||
setShouldShouldIconButtons(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShouldShouldIconButtons(false);
|
||||
});
|
||||
resizeObserver.observe(resizeObserverRef.current);
|
||||
return () => resizeObserver.disconnect(); // clean up
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" w="full" h="full" gap={4}>
|
||||
<Flex
|
||||
ref={resizeObserverRef}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
isAttached
|
||||
w="max-content"
|
||||
justifyContent="stretch"
|
||||
>
|
||||
{shouldShouldIconButtons ? (
|
||||
<>
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showGenerations')}
|
||||
tooltip={t('gallery.showGenerations')}
|
||||
isChecked={currentCategory === 'result'}
|
||||
icon={<FaImage />}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showUploads')}
|
||||
tooltip={t('gallery.showUploads')}
|
||||
isChecked={currentCategory === 'user'}
|
||||
icon={<FaUser />}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'result'}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.generations')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'user'}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.uploads')}
|
||||
</IAIButton>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
<Flex gap={2}>
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
icon={<FaWrench />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAISlider
|
||||
value={galleryImageMinimumWidth}
|
||||
onChange={handleChangeGalleryImageMinimumWidth}
|
||||
min={32}
|
||||
max={256}
|
||||
hideTooltip={true}
|
||||
label={t('gallery.galleryImageSize')}
|
||||
withReset
|
||||
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||
/>
|
||||
<IAICheckbox
|
||||
label={t('gallery.maintainAspectRatio')}
|
||||
isChecked={galleryImageObjectFit === 'contain'}
|
||||
onChange={() =>
|
||||
dispatch(
|
||||
setGalleryImageObjectFit(
|
||||
galleryImageObjectFit === 'contain' ? 'cover' : 'contain'
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
label={t('gallery.autoSwitchNewImages')}
|
||||
isChecked={shouldAutoSwitchToNewImages}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
label={t('gallery.singleColumnLayout')}
|
||||
isChecked={shouldUseSingleGalleryColumn}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseSingleGalleryColumn(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('gallery.pinGallery')}
|
||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Scrollable>
|
||||
<Flex direction="column" gap={2} h="full">
|
||||
{images.length || areMoreImagesAvailable ? (
|
||||
<>
|
||||
<Grid
|
||||
gap={2}
|
||||
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
||||
>
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
<IAIButton
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
flexShrink={0}
|
||||
>
|
||||
{areMoreImagesAvailable
|
||||
? t('gallery.loadMore')
|
||||
: t('gallery.allImagesLoaded')}
|
||||
</IAIButton>
|
||||
</>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
padding: 8,
|
||||
h: '100%',
|
||||
w: '100%',
|
||||
color: 'base.500',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
as={MdPhotoLibrary}
|
||||
sx={{
|
||||
w: 16,
|
||||
h: 16,
|
||||
}}
|
||||
/>
|
||||
<Text textAlign="center">{t('gallery.noImagesInGallery')}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Scrollable>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGalleryContent;
|
@ -0,0 +1,202 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setGalleryImageMinimumWidth,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
|
||||
import { clamp, isEqual } from 'lodash';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import './ImageGallery.css';
|
||||
import ImageGalleryContent from './ImageGalleryContent';
|
||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
||||
import {
|
||||
setShouldShowGallery,
|
||||
toggleGalleryPanel,
|
||||
togglePinGalleryPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
|
||||
const GALLERY_TAB_WIDTHS: Record<
|
||||
InvokeTabName,
|
||||
{ galleryMinWidth: number; galleryMaxWidth: number }
|
||||
> = {
|
||||
txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
|
||||
nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
};
|
||||
|
||||
const galleryPanelSelector = createSelector(
|
||||
[
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
gallerySelector,
|
||||
isStagingSelector,
|
||||
lightboxSelector,
|
||||
],
|
||||
(activeTabName, ui, gallery, isStaging, lightbox) => {
|
||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
||||
const { galleryImageMinimumWidth } = gallery;
|
||||
const { isLightboxOpen } = lightbox;
|
||||
|
||||
return {
|
||||
activeTabName,
|
||||
isStaging,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
isResizable: activeTabName !== 'unifiedCanvas',
|
||||
isLightboxOpen,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function ImageGalleryPanel() {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
activeTabName,
|
||||
isStaging,
|
||||
isResizable,
|
||||
isLightboxOpen,
|
||||
} = useAppSelector(galleryPanelSelector);
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(togglePinGalleryPanel());
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
const handleToggleGallery = () => {
|
||||
dispatch(toggleGalleryPanel());
|
||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'g',
|
||||
() => {
|
||||
handleToggleGallery();
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage());
|
||||
},
|
||||
{
|
||||
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
|
||||
},
|
||||
[isStaging, activeTabName]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage());
|
||||
},
|
||||
{
|
||||
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
|
||||
},
|
||||
[isStaging, activeTabName]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+g',
|
||||
() => {
|
||||
handleSetShouldPinGallery();
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinGallery,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
const IMAGE_SIZE_STEP = 32;
|
||||
|
||||
useHotkeys(
|
||||
'shift+up',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth < 256) {
|
||||
const newMinWidth = clamp(
|
||||
galleryImageMinimumWidth + IMAGE_SIZE_STEP,
|
||||
32,
|
||||
256
|
||||
);
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+down',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth > 32) {
|
||||
const newMinWidth = clamp(
|
||||
galleryImageMinimumWidth - IMAGE_SIZE_STEP,
|
||||
32,
|
||||
256
|
||||
);
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
return (
|
||||
<ResizableDrawer
|
||||
direction="right"
|
||||
isResizable={isResizable || !shouldPinGallery}
|
||||
isOpen={shouldShowGallery}
|
||||
onClose={handleCloseGallery}
|
||||
isPinned={shouldPinGallery && !isLightboxOpen}
|
||||
minWidth={
|
||||
shouldPinGallery
|
||||
? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth
|
||||
: 200
|
||||
}
|
||||
maxWidth={
|
||||
shouldPinGallery
|
||||
? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ImageGalleryContent />
|
||||
</ResizableDrawer>
|
||||
);
|
||||
}
|
@ -45,7 +45,6 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
|
||||
|
||||
type MetadataItemProps = {
|
||||
isLink?: boolean;
|
||||
@ -115,7 +114,6 @@ const MetadataItem = ({
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: InvokeAI.Image;
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
// TODO: I don't know if this is needed.
|
||||
@ -130,345 +128,328 @@ const memoEqualityCheck = (
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(
|
||||
({ image, styleClass }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const setBothPrompts = useSetBothPrompts();
|
||||
const setBothPrompts = useSetBothPrompts();
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
dispatch(setShouldShowImageDetails(false));
|
||||
});
|
||||
useHotkeys('esc', () => {
|
||||
dispatch(setShouldShowImageDetails(false));
|
||||
});
|
||||
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const dreamPrompt = image?.dreamPrompt;
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const dreamPrompt = image?.dreamPrompt;
|
||||
|
||||
const {
|
||||
cfg_scale,
|
||||
fit,
|
||||
height,
|
||||
hires_fix,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
orig_path,
|
||||
perlin,
|
||||
postprocessing,
|
||||
prompt,
|
||||
sampler,
|
||||
seamless,
|
||||
seed,
|
||||
steps,
|
||||
strength,
|
||||
threshold,
|
||||
type,
|
||||
variations,
|
||||
width,
|
||||
} = metadata;
|
||||
const {
|
||||
cfg_scale,
|
||||
fit,
|
||||
height,
|
||||
hires_fix,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
orig_path,
|
||||
perlin,
|
||||
postprocessing,
|
||||
prompt,
|
||||
sampler,
|
||||
seamless,
|
||||
seed,
|
||||
steps,
|
||||
strength,
|
||||
threshold,
|
||||
type,
|
||||
variations,
|
||||
width,
|
||||
} = metadata;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const metadataJSON = JSON.stringify(image.metadata, null, 2);
|
||||
const metadataJSON = JSON.stringify(image.metadata, null, 2);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={styleClass}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
borderRadius: 'base',
|
||||
padding: 4,
|
||||
overflow: 'scroll',
|
||||
maxHeight: APP_METADATA_HEIGHT,
|
||||
height: '100%',
|
||||
zIndex: '10',
|
||||
backdropFilter: 'blur(10px)',
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
padding: 4,
|
||||
gap: 1,
|
||||
flexDirection: 'column',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
backdropFilter: 'blur(20px)',
|
||||
bg: 'whiteAlpha.600',
|
||||
_dark: {
|
||||
bg: 'blackAlpha.600',
|
||||
}}
|
||||
>
|
||||
<Flex gap={1} direction="column" width="100%">
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight="semibold">File:</Text>
|
||||
<Link href={image.url} isExternal maxW="calc(100% - 3rem)">
|
||||
{image.url.length > 64
|
||||
? image.url.substring(0, 64).concat('...')
|
||||
: image.url}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Flex>
|
||||
{Object.keys(metadata).length > 0 ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Generation type" value={type} />}
|
||||
{image.metadata?.model_weights && (
|
||||
<MetadataItem
|
||||
label="Model"
|
||||
value={image.metadata.model_weights}
|
||||
/>
|
||||
)}
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} />
|
||||
)}
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
labelPosition="top"
|
||||
value={
|
||||
typeof prompt === 'string' ? prompt : promptToString(prompt)
|
||||
}
|
||||
onClick={() => setBothPrompts(prompt)}
|
||||
/>
|
||||
)}
|
||||
{seed !== undefined && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
onClick={() => dispatch(setSeed(seed))}
|
||||
/>
|
||||
)}
|
||||
{threshold !== undefined && (
|
||||
<MetadataItem
|
||||
label="Noise Threshold"
|
||||
value={threshold}
|
||||
onClick={() => dispatch(setThreshold(threshold))}
|
||||
/>
|
||||
)}
|
||||
{perlin !== undefined && (
|
||||
<MetadataItem
|
||||
label="Perlin Noise"
|
||||
value={perlin}
|
||||
onClick={() => dispatch(setPerlin(perlin))}
|
||||
/>
|
||||
)}
|
||||
{sampler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onClick={() => dispatch(setSampler(sampler))}
|
||||
/>
|
||||
)}
|
||||
{steps && (
|
||||
<MetadataItem
|
||||
label="Steps"
|
||||
value={steps}
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
onClick={() => dispatch(setCfgScale(cfg_scale))}
|
||||
/>
|
||||
)}
|
||||
{variations && variations.length > 0 && (
|
||||
<MetadataItem
|
||||
label="Seed-weight pairs"
|
||||
value={seedWeightsToString(variations)}
|
||||
onClick={() =>
|
||||
dispatch(setSeedWeights(seedWeightsToString(variations)))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{seamless && (
|
||||
<MetadataItem
|
||||
label="Seamless"
|
||||
value={seamless}
|
||||
onClick={() => dispatch(setSeamless(seamless))}
|
||||
/>
|
||||
)}
|
||||
{hires_fix && (
|
||||
<MetadataItem
|
||||
label="High Resolution Optimization"
|
||||
value={hires_fix}
|
||||
onClick={() => dispatch(setHiresFix(hires_fix))}
|
||||
/>
|
||||
)}
|
||||
{width && (
|
||||
<MetadataItem
|
||||
label="Width"
|
||||
value={width}
|
||||
onClick={() => dispatch(setWidth(width))}
|
||||
/>
|
||||
)}
|
||||
{height && (
|
||||
<MetadataItem
|
||||
label="Height"
|
||||
value={height}
|
||||
onClick={() => dispatch(setHeight(height))}
|
||||
/>
|
||||
)}
|
||||
{init_image_path && (
|
||||
<MetadataItem
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImage(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
<MetadataItem
|
||||
label="Mask image"
|
||||
value={mask_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setMaskPath(mask_image_path))}
|
||||
/>
|
||||
)}
|
||||
{type === 'img2img' && strength && (
|
||||
<MetadataItem
|
||||
label="Image to image strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setImg2imgStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{fit && (
|
||||
<MetadataItem
|
||||
label="Image to image fit"
|
||||
value={fit}
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing && postprocessing.length > 0 && (
|
||||
<>
|
||||
<Heading size="sm">Postprocessing</Heading>
|
||||
{postprocessing.map(
|
||||
(
|
||||
postprocess: InvokeAI.PostProcessedImageMetadata,
|
||||
i: number
|
||||
) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength, denoise_str } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${
|
||||
i + 1
|
||||
}: Upscale (ESRGAN)`}</Text>
|
||||
<MetadataItem
|
||||
label="Scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingStrength(strength))
|
||||
}
|
||||
/>
|
||||
{denoise_str !== undefined && (
|
||||
<MetadataItem
|
||||
label="Denoising strength"
|
||||
value={denoise_str}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingDenoising(denoise_str))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${
|
||||
i + 1
|
||||
}: Face restoration (GFPGAN)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() => {
|
||||
dispatch(setFacetoolStrength(strength));
|
||||
dispatch(setFacetoolType('gfpgan'));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'codeformer') {
|
||||
const { strength, fidelity } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${
|
||||
i + 1
|
||||
}: Face restoration (Codeformer)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() => {
|
||||
dispatch(setFacetoolStrength(strength));
|
||||
dispatch(setFacetoolType('codeformer'));
|
||||
}}
|
||||
/>
|
||||
{fidelity && (
|
||||
<MetadataItem
|
||||
label="Fidelity"
|
||||
value={fidelity}
|
||||
onClick={() => {
|
||||
dispatch(setCodeformerFidelity(fidelity));
|
||||
dispatch(setFacetoolType('codeformer'));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dreamPrompt && (
|
||||
<MetadataItem
|
||||
withCopy
|
||||
label="Dream Prompt"
|
||||
value={dreamPrompt}
|
||||
/>
|
||||
)}
|
||||
<Flex gap={2} direction="column">
|
||||
<Flex gap={2}>
|
||||
<Tooltip label="Copy metadata JSON">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.copyMetadataJson')}
|
||||
icon={<FaCopy />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
fontSize={14}
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(metadataJSON)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text fontWeight="semibold">Metadata JSON:</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0,
|
||||
mr: 2,
|
||||
mb: 4,
|
||||
ml: 2,
|
||||
padding: 4,
|
||||
borderRadius: 'base',
|
||||
overflowX: 'scroll',
|
||||
wordBreak: 'break-all',
|
||||
bg: 'whiteAlpha.100',
|
||||
}}
|
||||
>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width="100%" pt={10}>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight="semibold">File:</Text>
|
||||
<Link href={image.url} isExternal maxW="calc(100% - 3rem)">
|
||||
{image.url.length > 64
|
||||
? image.url.substring(0, 64).concat('...')
|
||||
: image.url}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Flex>
|
||||
{Object.keys(metadata).length > 0 ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Generation type" value={type} />}
|
||||
{image.metadata?.model_weights && (
|
||||
<MetadataItem label="Model" value={image.metadata.model_weights} />
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
memoEqualityCheck
|
||||
);
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} />
|
||||
)}
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
labelPosition="top"
|
||||
value={
|
||||
typeof prompt === 'string' ? prompt : promptToString(prompt)
|
||||
}
|
||||
onClick={() => setBothPrompts(prompt)}
|
||||
/>
|
||||
)}
|
||||
{seed !== undefined && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
onClick={() => dispatch(setSeed(seed))}
|
||||
/>
|
||||
)}
|
||||
{threshold !== undefined && (
|
||||
<MetadataItem
|
||||
label="Noise Threshold"
|
||||
value={threshold}
|
||||
onClick={() => dispatch(setThreshold(threshold))}
|
||||
/>
|
||||
)}
|
||||
{perlin !== undefined && (
|
||||
<MetadataItem
|
||||
label="Perlin Noise"
|
||||
value={perlin}
|
||||
onClick={() => dispatch(setPerlin(perlin))}
|
||||
/>
|
||||
)}
|
||||
{sampler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onClick={() => dispatch(setSampler(sampler))}
|
||||
/>
|
||||
)}
|
||||
{steps && (
|
||||
<MetadataItem
|
||||
label="Steps"
|
||||
value={steps}
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
onClick={() => dispatch(setCfgScale(cfg_scale))}
|
||||
/>
|
||||
)}
|
||||
{variations && variations.length > 0 && (
|
||||
<MetadataItem
|
||||
label="Seed-weight pairs"
|
||||
value={seedWeightsToString(variations)}
|
||||
onClick={() =>
|
||||
dispatch(setSeedWeights(seedWeightsToString(variations)))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{seamless && (
|
||||
<MetadataItem
|
||||
label="Seamless"
|
||||
value={seamless}
|
||||
onClick={() => dispatch(setSeamless(seamless))}
|
||||
/>
|
||||
)}
|
||||
{hires_fix && (
|
||||
<MetadataItem
|
||||
label="High Resolution Optimization"
|
||||
value={hires_fix}
|
||||
onClick={() => dispatch(setHiresFix(hires_fix))}
|
||||
/>
|
||||
)}
|
||||
{width && (
|
||||
<MetadataItem
|
||||
label="Width"
|
||||
value={width}
|
||||
onClick={() => dispatch(setWidth(width))}
|
||||
/>
|
||||
)}
|
||||
{height && (
|
||||
<MetadataItem
|
||||
label="Height"
|
||||
value={height}
|
||||
onClick={() => dispatch(setHeight(height))}
|
||||
/>
|
||||
)}
|
||||
{init_image_path && (
|
||||
<MetadataItem
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImage(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
<MetadataItem
|
||||
label="Mask image"
|
||||
value={mask_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setMaskPath(mask_image_path))}
|
||||
/>
|
||||
)}
|
||||
{type === 'img2img' && strength && (
|
||||
<MetadataItem
|
||||
label="Image to image strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setImg2imgStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{fit && (
|
||||
<MetadataItem
|
||||
label="Image to image fit"
|
||||
value={fit}
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing && postprocessing.length > 0 && (
|
||||
<>
|
||||
<Heading size="sm">Postprocessing</Heading>
|
||||
{postprocessing.map(
|
||||
(
|
||||
postprocess: InvokeAI.PostProcessedImageMetadata,
|
||||
i: number
|
||||
) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength, denoise_str } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${i + 1}: Upscale (ESRGAN)`}</Text>
|
||||
<MetadataItem
|
||||
label="Scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingStrength(strength))
|
||||
}
|
||||
/>
|
||||
{denoise_str !== undefined && (
|
||||
<MetadataItem
|
||||
label="Denoising strength"
|
||||
value={denoise_str}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingDenoising(denoise_str))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${
|
||||
i + 1
|
||||
}: Face restoration (GFPGAN)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() => {
|
||||
dispatch(setFacetoolStrength(strength));
|
||||
dispatch(setFacetoolType('gfpgan'));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'codeformer') {
|
||||
const { strength, fidelity } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={8} gap={1} direction="column">
|
||||
<Text size="md">{`${
|
||||
i + 1
|
||||
}: Face restoration (Codeformer)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() => {
|
||||
dispatch(setFacetoolStrength(strength));
|
||||
dispatch(setFacetoolType('codeformer'));
|
||||
}}
|
||||
/>
|
||||
{fidelity && (
|
||||
<MetadataItem
|
||||
label="Fidelity"
|
||||
value={fidelity}
|
||||
onClick={() => {
|
||||
dispatch(setCodeformerFidelity(fidelity));
|
||||
dispatch(setFacetoolType('codeformer'));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dreamPrompt && (
|
||||
<MetadataItem withCopy label="Dream Prompt" value={dreamPrompt} />
|
||||
)}
|
||||
<Flex gap={2} direction="column">
|
||||
<Flex gap={2}>
|
||||
<Tooltip label="Copy metadata JSON">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.copyMetadataJson')}
|
||||
icon={<FaCopy />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
fontSize={14}
|
||||
onClick={() => navigator.clipboard.writeText(metadataJSON)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text fontWeight="semibold">Metadata JSON:</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0,
|
||||
mr: 2,
|
||||
mb: 4,
|
||||
ml: 2,
|
||||
padding: 4,
|
||||
borderRadius: 'base',
|
||||
overflowX: 'scroll',
|
||||
wordBreak: 'break-all',
|
||||
bg: 'whiteAlpha.500',
|
||||
_dark: { bg: 'blackAlpha.500' },
|
||||
}}
|
||||
>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width="100%" pt={10}>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
ImageMetadataViewer.displayName = 'ImageMetadataViewer';
|
||||
|
||||
|
@ -1,53 +1,47 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { GalleryState } from './gallerySlice';
|
||||
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[gallerySelector, lightboxSelector, isStagingSelector, activeTabNameSelector],
|
||||
(gallery: GalleryState, lightbox, isStaging, activeTabName) => {
|
||||
[gallerySelector, uiSelector, lightboxSelector, activeTabNameSelector],
|
||||
(gallery, ui, lightbox, activeTabName) => {
|
||||
const {
|
||||
categories,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
galleryWidth,
|
||||
shouldUseSingleGalleryColumn,
|
||||
} = gallery;
|
||||
|
||||
const { shouldPinGallery } = ui;
|
||||
|
||||
const { isLightboxOpen } = lightbox;
|
||||
|
||||
return {
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
galleryGridTemplateColumns: shouldUseSingleGalleryColumn
|
||||
? 'auto'
|
||||
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||
activeTabName,
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
currentCategory,
|
||||
images: categories[currentCategory].images,
|
||||
areMoreImagesAvailable:
|
||||
categories[currentCategory].areMoreImagesAvailable,
|
||||
currentCategory,
|
||||
galleryWidth,
|
||||
isLightboxOpen,
|
||||
isStaging,
|
||||
shouldEnableResize:
|
||||
isLightboxOpen ||
|
||||
(activeTabName === 'unifiedCanvas' && shouldPinGallery)
|
||||
@ -65,7 +59,7 @@ export const imageGallerySelector = createSelector(
|
||||
|
||||
export const hoverableImageSelector = createSelector(
|
||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||
(gallery: GalleryState, system, lightbox, activeTabName) => {
|
||||
(gallery, system, lightbox, activeTabName) => {
|
||||
return {
|
||||
mayDeleteImage: system.isConnected && !system.isProcessing,
|
||||
galleryImageObjectFit: gallery.galleryImageObjectFit,
|
||||
|
@ -29,11 +29,8 @@ export interface GalleryState {
|
||||
boundingBox?: IRect;
|
||||
generationMode?: InvokeTabName;
|
||||
};
|
||||
shouldPinGallery: boolean;
|
||||
shouldShowGallery: boolean;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldHoldGalleryOpen: boolean;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
categories: {
|
||||
user: Gallery;
|
||||
@ -46,11 +43,8 @@ export interface GalleryState {
|
||||
|
||||
const initialState: GalleryState = {
|
||||
currentImageUuid: '',
|
||||
shouldPinGallery: true,
|
||||
shouldShowGallery: true,
|
||||
galleryImageMinimumWidth: 64,
|
||||
galleryImageObjectFit: 'cover',
|
||||
shouldHoldGalleryOpen: false,
|
||||
shouldAutoSwitchToNewImages: true,
|
||||
currentCategory: 'result',
|
||||
categories: {
|
||||
@ -233,13 +227,6 @@ export const gallerySlice = createSlice({
|
||||
areMoreImagesAvailable;
|
||||
}
|
||||
},
|
||||
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldPinGallery = action.payload;
|
||||
},
|
||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowGallery = action.payload;
|
||||
},
|
||||
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
@ -249,9 +236,6 @@ export const gallerySlice = createSlice({
|
||||
) => {
|
||||
state.galleryImageObjectFit = action.payload;
|
||||
},
|
||||
setShouldHoldGalleryOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldHoldGalleryOpen = action.payload;
|
||||
},
|
||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldAutoSwitchToNewImages = action.payload;
|
||||
},
|
||||
@ -279,11 +263,8 @@ export const {
|
||||
setIntermediateImage,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setShouldPinGallery,
|
||||
setShouldShowGallery,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldHoldGalleryOpen,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setCurrentCategory,
|
||||
setGalleryWidth,
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons';
|
||||
import ImageGallery from 'features/gallery/components/ImageGallery';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BiExit } from 'react-icons/bi';
|
||||
import { TransformWrapper } from 'react-zoom-pan-pinch';
|
||||
import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants';
|
||||
import useImageTransform from '../hooks/useImageTransform';
|
||||
import ReactPanZoomButtons from './ReactPanZoomButtons';
|
||||
import ReactPanZoomImage from './ReactPanZoomImage';
|
||||
@ -39,7 +39,6 @@ export const lightboxSelector = createSelector(
|
||||
|
||||
export default function Lightbox() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isLightBoxOpen = useAppSelector(
|
||||
(state: RootState) => state.lightbox.isLightboxOpen
|
||||
);
|
||||
@ -67,106 +66,102 @@ export default function Lightbox() {
|
||||
);
|
||||
|
||||
return (
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
minScale={0.1}
|
||||
initialPositionX={50}
|
||||
initialPositionY={50}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
insetInlineStart: 0,
|
||||
top: 0,
|
||||
zIndex: 30,
|
||||
animation: 'popIn 0.3s ease-in',
|
||||
bg: 'base.800',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
insetInlineStart: 4,
|
||||
gap: 4,
|
||||
zIndex: 3,
|
||||
<AnimatePresence>
|
||||
{isLightBoxOpen && (
|
||||
<motion.div
|
||||
key="lightbox"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100vw',
|
||||
height: `calc(100vh - ${PROGRESS_BAR_THICKNESS * 4}px)`,
|
||||
position: 'fixed',
|
||||
top: `${PROGRESS_BAR_THICKNESS * 4}px`,
|
||||
background: 'var(--invokeai-colors-base-900)',
|
||||
zIndex: 99,
|
||||
}}
|
||||
>
|
||||
<IAIIconButton
|
||||
icon={<BiExit />}
|
||||
aria-label={t('accessibility.exitViewer')}
|
||||
onClick={() => {
|
||||
dispatch(setIsLightboxOpen(false));
|
||||
}}
|
||||
fontSize={20}
|
||||
/>
|
||||
<ReactPanZoomButtons
|
||||
flipHorizontally={flipHorizontally}
|
||||
flipVertically={flipVertically}
|
||||
rotateCounterClockwise={rotateCounterClockwise}
|
||||
rotateClockwise={rotateClockwise}
|
||||
reset={reset}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<Grid
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
gridTemplateColumns: 'auto max-content',
|
||||
placeItems: 'center',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: 'base.850',
|
||||
}}
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
minScale={0.1}
|
||||
initialPositionX={50}
|
||||
initialPositionY={50}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
position: 'absolute',
|
||||
insetInlineStart: 4,
|
||||
gap: 4,
|
||||
zIndex: 3,
|
||||
top: 4,
|
||||
}}
|
||||
>
|
||||
<IAIIconButton
|
||||
icon={<BiExit />}
|
||||
aria-label="Exit Viewer"
|
||||
className="lightbox-close-btn"
|
||||
onClick={() => {
|
||||
dispatch(setIsLightboxOpen(false));
|
||||
}}
|
||||
fontSize={20}
|
||||
/>
|
||||
<ReactPanZoomButtons
|
||||
flipHorizontally={flipHorizontally}
|
||||
flipVertically={flipVertically}
|
||||
rotateCounterClockwise={rotateCounterClockwise}
|
||||
rotateClockwise={rotateClockwise}
|
||||
reset={reset}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
zIndex: 3,
|
||||
insetInlineStart: '50%',
|
||||
transform: 'translate(-50%, 0)',
|
||||
}}
|
||||
>
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
|
||||
{viewerImageToDisplay && (
|
||||
<>
|
||||
<ReactPanZoomImage
|
||||
rotation={rotation}
|
||||
scaleX={scaleX}
|
||||
scaleY={scaleY}
|
||||
image={viewerImageToDisplay.url}
|
||||
image={viewerImageToDisplay}
|
||||
styleClass="lightbox-image"
|
||||
/>
|
||||
{shouldShowImageDetails && (
|
||||
<ImageMetadataViewer image={viewerImageToDisplay} />
|
||||
)}
|
||||
|
||||
{!shouldShowImageDetails && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: '100vw',
|
||||
h: '100vh',
|
||||
px: 16,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<NextPrevImageButtons />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!shouldShowImageDetails && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: `calc(100vw - ${8 * 2 * 4}px)`,
|
||||
h: '100vh',
|
||||
mx: 8,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<NextPrevImageButtons />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
}}
|
||||
>
|
||||
<CurrentImageButtons />
|
||||
</Box>
|
||||
</Grid>
|
||||
<ImageGallery />
|
||||
</Flex>
|
||||
</Box>
|
||||
</TransformWrapper>
|
||||
</TransformWrapper>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
type ReactPanZoomProps = {
|
||||
image: string;
|
||||
image: InvokeAI.Image;
|
||||
styleClass?: string;
|
||||
alt?: string;
|
||||
ref?: React.Ref<HTMLImageElement>;
|
||||
@ -34,7 +35,7 @@ export default function ReactPanZoomImage({
|
||||
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
width: '100%',
|
||||
}}
|
||||
src={image}
|
||||
src={image.url}
|
||||
alt={alt}
|
||||
ref={ref}
|
||||
className={styleClass ? styleClass : ''}
|
||||
|
@ -59,6 +59,11 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={handleChangeAccordionState}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{renderAccordions()}
|
||||
</Accordion>
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
setCancelType,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, memo } from 'react';
|
||||
import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -44,9 +44,9 @@ interface CancelButtonProps {
|
||||
btnGroupWidth?: string | number;
|
||||
}
|
||||
|
||||
export default function CancelButton(
|
||||
const CancelButton = (
|
||||
props: CancelButtonProps & Omit<IAIIconButtonProps, 'aria-label'>
|
||||
) {
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { btnGroupWidth = 'auto', ...rest } = props;
|
||||
const {
|
||||
@ -146,4 +146,6 @@ export default function CancelButton(
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(CancelButton);
|
||||
|
@ -329,69 +329,71 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.appHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(appHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.appHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(appHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.generalHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(generalHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.generalHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(generalHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.galleryHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(galleryHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.galleryHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(galleryHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.unifiedCanvasHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(unifiedCanvasHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{t('hotkeys.unifiedCanvasHotkeys')}</h2>
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(unifiedCanvasHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Flex>
|
||||
</Accordion>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
|
@ -11,8 +11,6 @@ import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Text,
|
||||
@ -28,6 +26,8 @@ import type { RootState } from 'app/store';
|
||||
import type { FieldInputProps, FormikProps } from 'formik';
|
||||
import { isEqual, pickBy } from 'lodash';
|
||||
import ModelConvert from './ModelConvert';
|
||||
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
|
||||
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector],
|
||||
@ -139,11 +139,13 @@ export default function CheckpointModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.description && touched.description ? (
|
||||
<FormErrorMessage>{errors.description}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.description}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.descriptionValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -165,11 +167,11 @@ export default function CheckpointModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.config && touched.config ? (
|
||||
<FormErrorMessage>{errors.config}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>{errors.config}</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.configValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -191,11 +193,13 @@ export default function CheckpointModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.weights && touched.weights ? (
|
||||
<FormErrorMessage>{errors.weights}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.weights}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.modelLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -214,11 +218,11 @@ export default function CheckpointModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.vae && touched.vae ? (
|
||||
<FormErrorMessage>{errors.vae}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>{errors.vae}</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.vaeLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -253,11 +257,13 @@ export default function CheckpointModelEdit() {
|
||||
</Field>
|
||||
|
||||
{!!errors.width && touched.width ? (
|
||||
<FormErrorMessage>{errors.width}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.width}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.widthValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -291,11 +297,13 @@ export default function CheckpointModelEdit() {
|
||||
</Field>
|
||||
|
||||
{!!errors.height && touched.height ? (
|
||||
<FormErrorMessage>{errors.height}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.height}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.heightValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
@ -7,15 +7,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { Flex, FormControl, FormLabel, Text, VStack } from '@chakra-ui/react';
|
||||
|
||||
import { addNewModel } from 'app/socketio/actions';
|
||||
import { Field, Formik } from 'formik';
|
||||
@ -24,6 +16,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import type { InvokeDiffusersModelConfigProps } from 'app/invokeai';
|
||||
import type { RootState } from 'app/store';
|
||||
import { isEqual, pickBy } from 'lodash';
|
||||
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
|
||||
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector],
|
||||
@ -141,11 +135,13 @@ export default function DiffusersModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.description && touched.description ? (
|
||||
<FormErrorMessage>{errors.description}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.description}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.descriptionValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -167,11 +163,11 @@ export default function DiffusersModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.path && touched.path ? (
|
||||
<FormErrorMessage>{errors.path}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>{errors.path}</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.modelLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -190,11 +186,13 @@ export default function DiffusersModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.repo_id && touched.repo_id ? (
|
||||
<FormErrorMessage>{errors.repo_id}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.repo_id}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.repoIDValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -215,11 +213,13 @@ export default function DiffusersModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.vae?.path && touched.vae?.path ? (
|
||||
<FormErrorMessage>{errors.vae?.path}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.vae?.path}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.vaeLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
@ -240,11 +240,13 @@ export default function DiffusersModelEdit() {
|
||||
width="full"
|
||||
/>
|
||||
{!!errors.vae?.repo_id && touched.vae?.repo_id ? (
|
||||
<FormErrorMessage>{errors.vae?.repo_id}</FormErrorMessage>
|
||||
<IAIFormErrorMessage>
|
||||
{errors.vae?.repo_id}
|
||||
</IAIFormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
<IAIFormHelperText>
|
||||
{t('modelManager.vaeRepoIDValidationMsg')}
|
||||
</FormHelperText>
|
||||
</IAIFormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
@ -34,9 +34,6 @@ export default function ThemeChanger() {
|
||||
Object.keys(THEMES).forEach((theme) => {
|
||||
themesToRender.push(
|
||||
<IAIButton
|
||||
sx={{
|
||||
width: 24,
|
||||
}}
|
||||
isChecked={currentTheme === theme}
|
||||
leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
|
||||
size="sm"
|
||||
|
@ -1,22 +1,38 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { setShouldShowGallery } from 'features/gallery/store/gallerySlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
||||
import { isEqual } from 'lodash';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { floatingSelector } from './FloatingParametersPanelButtons';
|
||||
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
|
||||
|
||||
const floatingGalleryButtonSelector = createSelector(
|
||||
[activeTabNameSelector, uiSelector],
|
||||
(activeTabName, ui) => {
|
||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
||||
|
||||
return {
|
||||
shouldPinGallery,
|
||||
shouldShowGalleryButton:
|
||||
(!shouldPinGallery || !shouldShowGallery) &&
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName),
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
const FloatingGalleryButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { shouldPinGallery, shouldShowGalleryButton } = useAppSelector(
|
||||
floatingGalleryButtonSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const { shouldShowGalleryButton, shouldPinGallery } =
|
||||
useAppSelector(floatingSelector);
|
||||
|
||||
const handleShowGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
if (shouldPinGallery) {
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
}
|
||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
return shouldShowGalleryButton ? (
|
||||
|
@ -2,9 +2,7 @@ import { ChakraProps, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import { GalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
||||
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
||||
import {
|
||||
@ -22,46 +20,31 @@ const floatingButtonStyles: ChakraProps['sx'] = {
|
||||
borderEndStartRadius: 0,
|
||||
};
|
||||
|
||||
export const floatingSelector = createSelector(
|
||||
[gallerySelector, uiSelector, activeTabNameSelector],
|
||||
(gallery: GalleryState, ui, activeTabName) => {
|
||||
export const floatingParametersPanelButtonSelector = createSelector(
|
||||
[uiSelector, activeTabNameSelector],
|
||||
(ui, activeTabName) => {
|
||||
const {
|
||||
shouldPinParametersPanel,
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldUseCanvasBetaLayout,
|
||||
shouldShowParametersPanel,
|
||||
} = ui;
|
||||
|
||||
const { shouldShowGallery, shouldPinGallery, shouldHoldGalleryOpen } =
|
||||
gallery;
|
||||
|
||||
const canvasBetaLayoutCheck =
|
||||
shouldUseCanvasBetaLayout && activeTabName === 'unifiedCanvas';
|
||||
|
||||
const shouldShowParametersPanelButton =
|
||||
!canvasBetaLayoutCheck &&
|
||||
!(
|
||||
shouldShowParametersPanel ||
|
||||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
|
||||
) &&
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
const shouldShowGalleryButton =
|
||||
!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) &&
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
const shouldShowProcessButtons =
|
||||
!canvasBetaLayoutCheck &&
|
||||
(!shouldPinParametersPanel || !shouldShowParametersPanel);
|
||||
|
||||
const shouldShowParametersPanelButton =
|
||||
!canvasBetaLayoutCheck &&
|
||||
(!shouldPinParametersPanel || !shouldShowParametersPanel) &&
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
shouldShowProcessButtons,
|
||||
shouldShowParametersPanelButton,
|
||||
shouldShowParametersPanel,
|
||||
shouldShowGallery,
|
||||
shouldPinGallery,
|
||||
shouldShowGalleryButton,
|
||||
shouldShowProcessButtons,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
@ -71,16 +54,14 @@ const FloatingParametersPanelButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
shouldShowParametersPanelButton,
|
||||
shouldShowProcessButtons,
|
||||
shouldShowParametersPanelButton,
|
||||
shouldPinParametersPanel,
|
||||
} = useAppSelector(floatingSelector);
|
||||
} = useAppSelector(floatingParametersPanelButtonSelector);
|
||||
|
||||
const handleShowOptionsPanel = () => {
|
||||
dispatch(setShouldShowParametersPanel(true));
|
||||
if (shouldPinParametersPanel) {
|
||||
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
|
||||
}
|
||||
shouldPinParametersPanel && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
return shouldShowParametersPanelButton ? (
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
|
||||
export default function InitialImageOverlay() {
|
||||
const initialImage = useAppSelector(
|
||||
(state: RootState) => state.generation.initialImage
|
||||
);
|
||||
|
||||
return initialImage ? (
|
||||
<Image
|
||||
fit="contain"
|
||||
src={typeof initialImage === 'string' ? initialImage : initialImage.url}
|
||||
rounded="md"
|
||||
className="checkerboard"
|
||||
/>
|
||||
) : null;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import ImageToImageDisplay from './ImageToImageDisplay';
|
||||
import ImageToImagePanel from './ImageToImagePanel';
|
||||
|
||||
export default function ImageToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<ImageToImagePanel />}>
|
||||
<ImageToImageDisplay />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
.ltr-parameters-panel-transition-enter {
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-exit-active {
|
||||
transform: translateX(-150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-enter {
|
||||
transform: translateX(150%);
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-exit-active {
|
||||
transform: translateX(150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
import { Box, Flex, Tooltip, Icon, useTheme } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import {
|
||||
setShouldHoldParametersPanelOpen,
|
||||
setShouldPinParametersPanel,
|
||||
setShouldShowParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
|
||||
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { setParametersPanelScrollPosition } from 'features/ui/store/uiSlice';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { uiSelector } from '../store/uiSelectors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
APP_CONTENT_HEIGHT,
|
||||
OPTIONS_BAR_MAX_WIDTH,
|
||||
PROGRESS_BAR_THICKNESS,
|
||||
} from 'theme/util/constants';
|
||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||
|
||||
import './InvokeParametersPanel.css';
|
||||
import { no_scrollbar } from 'theme/components/scrollbar';
|
||||
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const optionsPanelSelector = createSelector(
|
||||
uiSelector,
|
||||
(ui) => {
|
||||
const {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
parametersPanelScrollPosition,
|
||||
} = ui;
|
||||
|
||||
return {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
parametersPanelScrollPosition,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const InvokeOptionsPanel = (props: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { direction } = useTheme();
|
||||
|
||||
const {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
} = useAppSelector(optionsPanelSelector);
|
||||
|
||||
const optionsPanelRef = useRef<HTMLDivElement>(null);
|
||||
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Hotkeys
|
||||
useHotkeys(
|
||||
'o',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
|
||||
shouldPinParametersPanel &&
|
||||
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
|
||||
},
|
||||
[shouldShowParametersPanel, shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinParametersPanel,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
handleClickPinOptionsPanel();
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
const handleCloseOptionsPanel = useCallback(() => {
|
||||
if (shouldPinParametersPanel) return;
|
||||
dispatch(
|
||||
setParametersPanelScrollPosition(
|
||||
optionsPanelContainerRef.current
|
||||
? optionsPanelContainerRef.current.scrollTop
|
||||
: 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
dispatch(setShouldHoldParametersPanelOpen(false));
|
||||
}, [dispatch, shouldPinParametersPanel]);
|
||||
|
||||
const setCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current = window.setTimeout(
|
||||
() => handleCloseOptionsPanel(),
|
||||
500
|
||||
);
|
||||
};
|
||||
|
||||
const cancelCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
};
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (
|
||||
optionsPanelRef.current &&
|
||||
!optionsPanelRef.current.contains(e.target as Node)
|
||||
) {
|
||||
handleCloseOptionsPanel();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleCloseOptionsPanel]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={optionsPanelRef}
|
||||
in={
|
||||
shouldShowParametersPanel ||
|
||||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
|
||||
}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames={`${direction}-parameters-panel-transition`}
|
||||
>
|
||||
<Box
|
||||
className={`${direction}-parameters-panel-transition`}
|
||||
tabIndex={1}
|
||||
ref={optionsPanelRef}
|
||||
onMouseEnter={
|
||||
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
onMouseOver={
|
||||
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
sx={{
|
||||
borderInlineEndWidth: !shouldPinParametersPanel ? 5 : 0,
|
||||
borderInlineEndStyle: 'solid',
|
||||
bg: 'base.900',
|
||||
borderColor: 'base.700',
|
||||
height: APP_CONTENT_HEIGHT,
|
||||
width: OPTIONS_BAR_MAX_WIDTH,
|
||||
maxWidth: OPTIONS_BAR_MAX_WIDTH,
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
...no_scrollbar,
|
||||
...(!shouldPinParametersPanel && {
|
||||
zIndex: 20,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
width: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
|
||||
maxWidth: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
|
||||
height: '100%',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box sx={{ margin: !shouldPinParametersPanel && 4 }}>
|
||||
<Flex
|
||||
ref={optionsPanelContainerRef}
|
||||
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target !== optionsPanelContainerRef.current) {
|
||||
cancelCloseOptionsPanelTimer();
|
||||
} else {
|
||||
!shouldPinParametersPanel && setCloseOptionsPanelTimer();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t('common.pinOptionsPanel')}>
|
||||
<Box
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
padding: 2,
|
||||
top: 4,
|
||||
insetInlineEnd: 4,
|
||||
zIndex: 20,
|
||||
...(shouldPinParametersPanel && {
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{ opacity: 0.2 }}
|
||||
as={shouldPinParametersPanel ? BsPinAngleFill : BsPinAngle}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{!shouldPinParametersPanel && (
|
||||
<Box sx={{ pt: PROGRESS_BAR_THICKNESS, pb: 2 }}>
|
||||
<InvokeAILogoComponent />
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeOptionsPanel;
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ChakraProps,
|
||||
Icon,
|
||||
Tab,
|
||||
TabList,
|
||||
@ -13,18 +14,10 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import NodesWIP from 'common/components/WorkInProgress/NodesWIP';
|
||||
import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP';
|
||||
import TrainingWIP from 'common/components/WorkInProgress/Training';
|
||||
import useUpdateTranslations from 'common/hooks/useUpdateTranslations';
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { setShouldShowGallery } from 'features/gallery/store/gallerySlice';
|
||||
import Lightbox from 'features/lightbox/components/Lightbox';
|
||||
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import {
|
||||
setActiveTab,
|
||||
setShouldShowParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import i18n from 'i18n';
|
||||
import { ReactElement } from 'react';
|
||||
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
MdDeviceHub,
|
||||
@ -35,58 +28,55 @@ import {
|
||||
MdTextFields,
|
||||
} from 'react-icons/md';
|
||||
import { activeTabIndexSelector } from '../store/uiSelectors';
|
||||
import { floatingSelector } from './FloatingParametersPanelButtons';
|
||||
import ImageToImageWorkarea from './ImageToImage';
|
||||
import TextToImageWorkarea from './TextToImage';
|
||||
import UnifiedCanvasWorkarea from './UnifiedCanvas/UnifiedCanvasWorkarea';
|
||||
import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea';
|
||||
import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea';
|
||||
import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResourceKey } from 'i18next';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
|
||||
export interface InvokeTabInfo {
|
||||
title: ReactElement;
|
||||
workarea: ReactElement;
|
||||
tooltip: string;
|
||||
id: InvokeTabName;
|
||||
icon: ReactNode;
|
||||
workarea: ReactNode;
|
||||
}
|
||||
|
||||
export const tabDict: Record<InvokeTabName, InvokeTabInfo> = {
|
||||
txt2img: {
|
||||
title: <Icon as={MdTextFields} boxSize={6} />,
|
||||
workarea: <TextToImageWorkarea />,
|
||||
tooltip: 'Text To Image',
|
||||
},
|
||||
img2img: {
|
||||
title: <Icon as={MdPhotoLibrary} boxSize={6} />,
|
||||
workarea: <ImageToImageWorkarea />,
|
||||
tooltip: 'Image To Image',
|
||||
},
|
||||
unifiedCanvas: {
|
||||
title: <Icon as={MdGridOn} boxSize={6} />,
|
||||
workarea: <UnifiedCanvasWorkarea />,
|
||||
tooltip: 'Unified Canvas',
|
||||
},
|
||||
nodes: {
|
||||
title: <Icon as={MdDeviceHub} boxSize={6} />,
|
||||
workarea: <NodesWIP />,
|
||||
tooltip: 'Nodes',
|
||||
},
|
||||
postprocess: {
|
||||
title: <Icon as={MdPhotoFilter} boxSize={6} />,
|
||||
workarea: <PostProcessingWIP />,
|
||||
tooltip: 'Post Processing',
|
||||
},
|
||||
training: {
|
||||
title: <Icon as={MdFlashOn} boxSize={6} />,
|
||||
workarea: <TrainingWIP />,
|
||||
tooltip: 'Training',
|
||||
},
|
||||
const tabIconStyles: ChakraProps['sx'] = {
|
||||
boxSize: 6,
|
||||
};
|
||||
|
||||
function updateTabTranslations() {
|
||||
tabDict.txt2img.tooltip = i18n.t('common.text2img');
|
||||
tabDict.img2img.tooltip = i18n.t('common.img2img');
|
||||
tabDict.unifiedCanvas.tooltip = i18n.t('common.unifiedCanvas');
|
||||
tabDict.nodes.tooltip = i18n.t('common.nodes');
|
||||
tabDict.postprocess.tooltip = i18n.t('common.postProcessing');
|
||||
tabDict.training.tooltip = i18n.t('common.training');
|
||||
}
|
||||
const tabInfo: InvokeTabInfo[] = [
|
||||
{
|
||||
id: 'txt2img',
|
||||
icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
|
||||
workarea: <TextToImageWorkarea />,
|
||||
},
|
||||
{
|
||||
id: 'img2img',
|
||||
icon: <Icon as={MdPhotoLibrary} sx={tabIconStyles} />,
|
||||
workarea: <ImageToImageWorkarea />,
|
||||
},
|
||||
{
|
||||
id: 'unifiedCanvas',
|
||||
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
|
||||
workarea: <UnifiedCanvasWorkarea />,
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
|
||||
workarea: <NodesWIP />,
|
||||
},
|
||||
{
|
||||
id: 'postprocessing',
|
||||
icon: <Icon as={MdPhotoFilter} sx={tabIconStyles} />,
|
||||
workarea: <PostProcessingWIP />,
|
||||
},
|
||||
{
|
||||
id: 'training',
|
||||
icon: <Icon as={MdFlashOn} sx={tabIconStyles} />,
|
||||
workarea: <TrainingWIP />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function InvokeTabs() {
|
||||
const activeTab = useAppSelector(activeTabIndexSelector);
|
||||
@ -95,14 +85,15 @@ export default function InvokeTabs() {
|
||||
(state: RootState) => state.lightbox.isLightboxOpen
|
||||
);
|
||||
|
||||
const {
|
||||
shouldShowGallery,
|
||||
shouldShowParametersPanel,
|
||||
shouldPinGallery,
|
||||
shouldPinParametersPanel,
|
||||
} = useAppSelector(floatingSelector);
|
||||
const shouldPinGallery = useAppSelector(
|
||||
(state: RootState) => state.ui.shouldPinGallery
|
||||
);
|
||||
|
||||
useUpdateTranslations(updateTabTranslations);
|
||||
const shouldPinParametersPanel = useAppSelector(
|
||||
(state: RootState) => state.ui.shouldPinParametersPanel
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -142,53 +133,50 @@ export default function InvokeTabs() {
|
||||
useHotkeys(
|
||||
'f',
|
||||
() => {
|
||||
if (shouldShowGallery || shouldShowParametersPanel) {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
dispatch(setShouldShowGallery(false));
|
||||
} else {
|
||||
dispatch(setShouldShowParametersPanel(true));
|
||||
dispatch(setShouldShowGallery(true));
|
||||
}
|
||||
if (shouldPinGallery || shouldPinParametersPanel)
|
||||
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
|
||||
dispatch(togglePanels());
|
||||
(shouldPinGallery || shouldPinParametersPanel) &&
|
||||
dispatch(requestCanvasRescale());
|
||||
},
|
||||
[shouldShowGallery, shouldShowParametersPanel]
|
||||
[shouldPinGallery, shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
const renderTabs = () => {
|
||||
const tabsToRender: ReactElement[] = [];
|
||||
Object.keys(tabDict).forEach((key) => {
|
||||
tabsToRender.push(
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
tabInfo.map((tab) => (
|
||||
<Tooltip
|
||||
key={key}
|
||||
key={tab.id}
|
||||
hasArrow
|
||||
label={tabDict[key as keyof typeof tabDict].tooltip}
|
||||
label={String(t(`common.${tab.id}` as ResourceKey))}
|
||||
placement="end"
|
||||
>
|
||||
<Tab>
|
||||
<VisuallyHidden>
|
||||
{tabDict[key as keyof typeof tabDict].tooltip}
|
||||
{String(t(`common.${tab.id}` as ResourceKey))}
|
||||
</VisuallyHidden>
|
||||
{tabDict[key as keyof typeof tabDict].title}
|
||||
{tab.icon}
|
||||
</Tab>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
return tabsToRender;
|
||||
};
|
||||
)),
|
||||
[t]
|
||||
);
|
||||
|
||||
const renderTabPanels = () => {
|
||||
const tabPanelsToRender: ReactElement[] = [];
|
||||
Object.keys(tabDict).forEach((key) => {
|
||||
tabPanelsToRender.push(
|
||||
<TabPanel key={key}>
|
||||
{tabDict[key as keyof typeof tabDict].workarea}
|
||||
</TabPanel>
|
||||
);
|
||||
});
|
||||
return tabPanelsToRender;
|
||||
};
|
||||
const tabPanels = useMemo(
|
||||
() =>
|
||||
tabInfo.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* isLazy means the tabs are mounted and unmounted when changing them. There is a tradeoff here,
|
||||
* as mounting is expensive, but so is retaining all tabs in the DOM at all times.
|
||||
*
|
||||
* Removing isLazy messes with the outside click watcher, which is used by ResizableDrawer.
|
||||
* Because you have multiple handlers listening for an outside click, any click anywhere triggers
|
||||
* the watcher for the hidden drawers, closing the open drawer.
|
||||
*
|
||||
* TODO: Add logic to the `useOutsideClick` in ResizableDrawer to enable it only for the active
|
||||
* tab's drawer.
|
||||
*/
|
||||
return (
|
||||
<Tabs
|
||||
isLazy
|
||||
@ -197,9 +185,10 @@ export default function InvokeTabs() {
|
||||
onChange={(index: number) => {
|
||||
dispatch(setActiveTab(index));
|
||||
}}
|
||||
flexGrow={1}
|
||||
>
|
||||
<TabList>{renderTabs()}</TabList>
|
||||
<TabPanels>{isLightBoxOpen ? <Lightbox /> : renderTabPanels()}</TabPanels>
|
||||
<TabList>{tabs}</TabList>
|
||||
<TabPanels>{tabPanels}</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import ImageGallery from 'features/gallery/components/ImageGallery';
|
||||
import { setInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
@ -11,17 +10,16 @@ import { DragEvent, ReactNode } from 'react';
|
||||
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
import { APP_CONTENT_HEIGHT } from 'theme/util/constants';
|
||||
import ParametersPanel from './ParametersPanel';
|
||||
|
||||
const workareaSelector = createSelector(
|
||||
[uiSelector, lightboxSelector, activeTabNameSelector],
|
||||
(ui, lightbox, activeTabName) => {
|
||||
[uiSelector, activeTabNameSelector],
|
||||
(ui, activeTabName) => {
|
||||
const { shouldPinParametersPanel } = ui;
|
||||
const { isLightboxOpen } = lightbox;
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
isLightboxOpen,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
@ -33,14 +31,14 @@ const workareaSelector = createSelector(
|
||||
);
|
||||
|
||||
type InvokeWorkareaProps = BoxProps & {
|
||||
optionsPanel: ReactNode;
|
||||
parametersPanelContent: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
const { parametersPanelContent, children, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { optionsPanel, children, ...rest } = props;
|
||||
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
|
||||
const { activeTabName } = useAppSelector(workareaSelector);
|
||||
|
||||
const getImageByUuid = useGetImageByUuid();
|
||||
|
||||
@ -56,15 +54,12 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box {...rest} pos="relative" w="100%" h="100%">
|
||||
<Flex gap={4} h="100%">
|
||||
{optionsPanel}
|
||||
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
|
||||
{children}
|
||||
</Box>
|
||||
{!isLightboxOpen && <ImageGallery />}
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex {...rest} pos="relative" w="full" h={APP_CONTENT_HEIGHT} gap={4}>
|
||||
<ParametersPanel>{parametersPanelContent}</ParametersPanel>
|
||||
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,129 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
||||
import {
|
||||
setShouldShowParametersPanel,
|
||||
toggleParametersPanel,
|
||||
togglePinParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||
import Scrollable from './common/Scrollable';
|
||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
|
||||
const parametersPanelSelector = createSelector(
|
||||
[uiSelector, activeTabNameSelector, lightboxSelector],
|
||||
(ui, activeTabName, lightbox) => {
|
||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
||||
const { isLightboxOpen } = lightbox;
|
||||
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
shouldShowParametersPanel,
|
||||
isResizable: activeTabName !== 'unifiedCanvas',
|
||||
isLightboxOpen,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type ParametersPanelProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ParametersPanel = ({ children }: ParametersPanelProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const {
|
||||
shouldPinParametersPanel,
|
||||
shouldShowParametersPanel,
|
||||
isResizable,
|
||||
isLightboxOpen,
|
||||
} = useAppSelector(parametersPanelSelector);
|
||||
|
||||
const closeParametersPanel = () => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'o',
|
||||
() => {
|
||||
dispatch(toggleParametersPanel());
|
||||
shouldPinParametersPanel && dispatch(requestCanvasRescale());
|
||||
},
|
||||
{ enabled: () => !isLightboxOpen },
|
||||
[shouldPinParametersPanel, isLightboxOpen]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinParametersPanel,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
dispatch(togglePinParametersPanel());
|
||||
dispatch(requestCanvasRescale());
|
||||
},
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<ResizableDrawer
|
||||
direction="left"
|
||||
isResizable={isResizable || !shouldPinParametersPanel}
|
||||
isOpen={shouldShowParametersPanel}
|
||||
onClose={closeParametersPanel}
|
||||
isPinned={shouldPinParametersPanel || isLightboxOpen}
|
||||
sx={{
|
||||
borderColor: 'base.700',
|
||||
p: shouldPinParametersPanel ? 0 : 4,
|
||||
bg: 'base.900',
|
||||
}}
|
||||
initialWidth={PARAMETERS_PANEL_WIDTH}
|
||||
minWidth={PARAMETERS_PANEL_WIDTH}
|
||||
>
|
||||
<Flex flexDir="column" position="relative" h="full" w="full">
|
||||
{!shouldPinParametersPanel && (
|
||||
<Flex
|
||||
paddingTop={1.5}
|
||||
paddingBottom={4}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<InvokeAILogoComponent />
|
||||
<PinParametersPanelButton />
|
||||
</Flex>
|
||||
)}
|
||||
<Scrollable>{children}</Scrollable>
|
||||
{shouldPinParametersPanel && (
|
||||
<PinParametersPanelButton
|
||||
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</ResizableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParametersPanel);
|
@ -0,0 +1,51 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
} from 'common/components/IAIIconButton';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { setShouldPinParametersPanel } from '../store/uiSlice';
|
||||
|
||||
type PinParametersPanelButtonProps = Omit<IAIIconButtonProps, 'aria-label'>;
|
||||
|
||||
const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
|
||||
const { sx } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldPinParametersPanel = useAppSelector(
|
||||
(state) => state.ui.shouldPinParametersPanel
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={t('common.pinOptionsPanel')}>
|
||||
<IAIIconButton
|
||||
{...props}
|
||||
aria-label={t('common.pinOptionsPanel')}
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
sx={{
|
||||
color: 'base.700',
|
||||
_hover: {
|
||||
color: 'base.550',
|
||||
},
|
||||
_active: {
|
||||
color: 'base.500',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinParametersPanelButton;
|
@ -1,11 +0,0 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import TextToImageDisplay from './TextToImageDisplay';
|
||||
import TextToImagePanel from './TextToImagePanel';
|
||||
|
||||
export default function TextToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
|
||||
<TextToImageDisplay />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import UnifiedCanvasDisplayBeta from './UnifiedCanvasBeta/UnifiedCanvasDisplayBeta';
|
||||
import UnifiedCanvasDisplay from './UnifiedCanvasDisplay';
|
||||
import UnifiedCanvasPanel from './UnifiedCanvasPanel';
|
||||
|
||||
export default function UnifiedCanvasWorkarea() {
|
||||
const shouldUseCanvasBetaLayout = useAppSelector(
|
||||
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
|
||||
);
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<UnifiedCanvasPanel />}>
|
||||
{shouldUseCanvasBetaLayout ? (
|
||||
<UnifiedCanvasDisplayBeta />
|
||||
) : (
|
||||
<UnifiedCanvasDisplay />
|
||||
)}
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
import {
|
||||
Box,
|
||||
chakra,
|
||||
ChakraProps,
|
||||
Slide,
|
||||
useOutsideClick,
|
||||
useTheme,
|
||||
SlideDirection,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Resizable,
|
||||
ResizableProps,
|
||||
ResizeCallback,
|
||||
ResizeStartCallback,
|
||||
} from 're-resizable';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { LangDirection } from './types';
|
||||
import {
|
||||
getHandleEnables,
|
||||
getMinMaxDimensions,
|
||||
getSlideDirection,
|
||||
getStyles,
|
||||
parseAndPadSize,
|
||||
} from './util';
|
||||
|
||||
type ResizableDrawerProps = ResizableProps & {
|
||||
children: ReactNode;
|
||||
isResizable: boolean;
|
||||
isPinned: boolean;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
direction?: SlideDirection;
|
||||
initialWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
initialHeight?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
onResizeStart?: ResizeStartCallback;
|
||||
onResizeStop?: ResizeCallback;
|
||||
onResize?: ResizeCallback;
|
||||
handleWidth?: string | number;
|
||||
handleInteractWidth?: string | number;
|
||||
sx?: ChakraProps['sx'];
|
||||
};
|
||||
|
||||
const ChakraResizeable = chakra(Resizable, {
|
||||
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
||||
});
|
||||
|
||||
const ResizableDrawer = ({
|
||||
direction = 'left',
|
||||
isResizable,
|
||||
isPinned,
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
initialWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
initialHeight,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
onResizeStart,
|
||||
onResizeStop,
|
||||
onResize,
|
||||
sx = {},
|
||||
}: ResizableDrawerProps) => {
|
||||
const langDirection = useTheme().direction as LangDirection;
|
||||
|
||||
const outsideClickRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const defaultWidth = useMemo(
|
||||
() =>
|
||||
initialWidth ??
|
||||
minWidth ??
|
||||
(['left', 'right'].includes(direction) ? 500 : '100%'),
|
||||
[initialWidth, minWidth, direction]
|
||||
);
|
||||
|
||||
const defaultHeight = useMemo(
|
||||
() =>
|
||||
initialHeight ??
|
||||
minHeight ??
|
||||
(['top', 'bottom'].includes(direction) ? 500 : '100%'),
|
||||
[initialHeight, minHeight, direction]
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState<number | string>(defaultWidth);
|
||||
|
||||
const [height, setHeight] = useState<number | string>(defaultHeight);
|
||||
|
||||
useOutsideClick({
|
||||
ref: outsideClickRef,
|
||||
handler: () => {
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleEnables = useMemo(
|
||||
() => (isResizable ? getHandleEnables({ direction, langDirection }) : {}),
|
||||
[isResizable, langDirection, direction]
|
||||
);
|
||||
|
||||
const minMaxDimensions = useMemo(
|
||||
() =>
|
||||
getMinMaxDimensions({
|
||||
direction,
|
||||
minWidth: isResizable
|
||||
? parseAndPadSize(minWidth, 18)
|
||||
: parseAndPadSize(minWidth),
|
||||
maxWidth: isResizable
|
||||
? parseAndPadSize(maxWidth, 18)
|
||||
: parseAndPadSize(maxWidth),
|
||||
minHeight: isResizable
|
||||
? parseAndPadSize(minHeight, 18)
|
||||
: parseAndPadSize(minHeight),
|
||||
maxHeight: isResizable
|
||||
? parseAndPadSize(maxHeight, 18)
|
||||
: parseAndPadSize(maxHeight),
|
||||
}),
|
||||
[minWidth, maxWidth, minHeight, maxHeight, direction, isResizable]
|
||||
);
|
||||
|
||||
const { containerStyles, handleStyles } = useMemo(
|
||||
() =>
|
||||
getStyles({
|
||||
isPinned,
|
||||
isResizable,
|
||||
direction,
|
||||
}),
|
||||
[isPinned, isResizable, direction]
|
||||
);
|
||||
|
||||
const slideDirection = useMemo(
|
||||
() => getSlideDirection(direction, langDirection),
|
||||
[direction, langDirection]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (['left', 'right'].includes(direction)) {
|
||||
setHeight(isPinned ? '100%' : '100vh');
|
||||
}
|
||||
if (['top', 'bottom'].includes(direction)) {
|
||||
setWidth(isPinned ? '100%' : '100vw');
|
||||
}
|
||||
}, [isPinned, direction]);
|
||||
|
||||
return (
|
||||
<Slide
|
||||
direction={slideDirection}
|
||||
in={isOpen}
|
||||
unmountOnExit={isPinned}
|
||||
motionProps={{ initial: isPinned }}
|
||||
{...(isPinned
|
||||
? {
|
||||
style: {
|
||||
position: undefined,
|
||||
left: undefined,
|
||||
right: undefined,
|
||||
top: undefined,
|
||||
bottom: undefined,
|
||||
width: undefined,
|
||||
},
|
||||
}
|
||||
: {
|
||||
// transition: { enter: { duration: 0.15 }, exit: { duration: 0.15 } },
|
||||
style: { zIndex: 99, width: 'full' },
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
ref={outsideClickRef}
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
}}
|
||||
>
|
||||
<ChakraResizeable
|
||||
size={{
|
||||
width: isResizable ? width : defaultWidth,
|
||||
height: isResizable ? height : defaultHeight,
|
||||
}}
|
||||
enable={handleEnables}
|
||||
handleStyles={handleStyles}
|
||||
{...minMaxDimensions}
|
||||
sx={{
|
||||
borderColor: 'base.800',
|
||||
p: isPinned ? 0 : 4,
|
||||
bg: 'base.900',
|
||||
height: 'full',
|
||||
boxShadow: !isPinned ? '0 0 4rem 0 rgba(0, 0, 0, 0.8)' : '',
|
||||
...containerStyles,
|
||||
...sx,
|
||||
}}
|
||||
onResizeStart={(event, direction, elementRef) => {
|
||||
onResizeStart && onResizeStart(event, direction, elementRef);
|
||||
}}
|
||||
onResize={(event, direction, elementRef, delta) => {
|
||||
onResize && onResize(event, direction, elementRef, delta);
|
||||
}}
|
||||
onResizeStop={(event, direction, elementRef, delta) => {
|
||||
if (['left', 'right'].includes(direction)) {
|
||||
setWidth(Number(width) + delta.width);
|
||||
}
|
||||
if (['top', 'bottom'].includes(direction)) {
|
||||
setHeight(Number(height) + delta.height);
|
||||
}
|
||||
onResizeStop && onResizeStop(event, direction, elementRef, delta);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChakraResizeable>
|
||||
</Box>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResizableDrawer;
|
@ -0,0 +1,2 @@
|
||||
export type Placement = 'top' | 'right' | 'bottom' | 'left';
|
||||
export type LangDirection = 'ltr' | 'rtl' | undefined;
|
@ -0,0 +1,294 @@
|
||||
import { SlideDirection } from '@chakra-ui/react';
|
||||
import { AnimationProps } from 'framer-motion';
|
||||
import { HandleStyles } from 're-resizable';
|
||||
import { CSSProperties } from 'react';
|
||||
import { LangDirection } from './types';
|
||||
|
||||
export type GetHandleEnablesOptions = {
|
||||
direction: SlideDirection;
|
||||
langDirection: LangDirection;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine handles to enable. `re-resizable` doesn't handle RTL, so we have to do that here.
|
||||
*/
|
||||
export const getHandleEnables = ({
|
||||
direction,
|
||||
langDirection,
|
||||
}: GetHandleEnablesOptions) => {
|
||||
const top = direction === 'bottom';
|
||||
|
||||
const right =
|
||||
(langDirection !== 'rtl' && direction === 'left') ||
|
||||
(langDirection === 'rtl' && direction === 'right');
|
||||
|
||||
const bottom = direction === 'top';
|
||||
|
||||
const left =
|
||||
(langDirection !== 'rtl' && direction === 'right') ||
|
||||
(langDirection === 'rtl' && direction === 'left');
|
||||
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
};
|
||||
};
|
||||
|
||||
export type GetDefaultSizeOptions = {
|
||||
initialWidth?: string | number;
|
||||
initialHeight?: string | number;
|
||||
direction: SlideDirection;
|
||||
};
|
||||
|
||||
// Get default sizes based on direction and initial values
|
||||
export const getDefaultSize = ({
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
direction,
|
||||
}: GetDefaultSizeOptions) => {
|
||||
const width =
|
||||
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
|
||||
|
||||
const height =
|
||||
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export type GetMinMaxDimensionsOptions = {
|
||||
direction: SlideDirection;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
};
|
||||
|
||||
// Get the min/max width/height based on direction and provided values
|
||||
export const getMinMaxDimensions = ({
|
||||
direction,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
}: GetMinMaxDimensionsOptions) => {
|
||||
const minW =
|
||||
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
|
||||
|
||||
const maxW =
|
||||
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
|
||||
|
||||
const minH =
|
||||
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
|
||||
|
||||
const maxH =
|
||||
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
|
||||
|
||||
return {
|
||||
...(minW ? { minWidth: minW } : {}),
|
||||
...(maxW ? { maxWidth: maxW } : {}),
|
||||
...(minH ? { minHeight: minH } : {}),
|
||||
...(maxH ? { maxHeight: maxH } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export type GetAnimationsOptions = {
|
||||
direction: SlideDirection;
|
||||
langDirection: LangDirection;
|
||||
};
|
||||
|
||||
// Get the framer-motion animation props, taking into account language direction
|
||||
export const getAnimations = ({
|
||||
direction,
|
||||
langDirection,
|
||||
}: GetAnimationsOptions): AnimationProps => {
|
||||
const baseAnimation = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
// chakra consumes the transition prop, which, for it, is a string.
|
||||
// however we know the transition prop will make it to framer motion,
|
||||
// which wants it as an object. cast as string to satisfy TS.
|
||||
transition: { duration: 0.2, ease: 'easeInOut' },
|
||||
};
|
||||
|
||||
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
|
||||
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { y: -999 },
|
||||
animate: { y: 0 },
|
||||
exit: { y: -999 },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { x: 999 * langDirectionFactor },
|
||||
animate: { x: 0 },
|
||||
exit: { x: 999 * langDirectionFactor },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'bottom') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { y: 999 },
|
||||
animate: { y: 0 },
|
||||
exit: { y: 999 },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { x: -999 * langDirectionFactor },
|
||||
animate: { x: 0 },
|
||||
exit: { x: -999 * langDirectionFactor },
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export type GetResizableStylesProps = {
|
||||
isPinned: boolean;
|
||||
isResizable: boolean;
|
||||
direction: SlideDirection;
|
||||
};
|
||||
|
||||
// Expand the handle hitbox
|
||||
const HANDLE_INTERACT_PADDING = '0.75rem';
|
||||
|
||||
// Visible padding around handle
|
||||
const HANDLE_PADDING = '1rem';
|
||||
|
||||
const HANDLE_WIDTH_PINNED = '2px';
|
||||
const HANDLE_WIDTH_UNPINNED = '5px';
|
||||
|
||||
// Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS
|
||||
export const getStyles = ({
|
||||
isPinned,
|
||||
isResizable,
|
||||
direction,
|
||||
}: GetResizableStylesProps): {
|
||||
containerStyles: CSSProperties; // technically this could be ChakraProps['sx'], but we cannot use this for HandleStyles so leave it as CSSProperties to be consistent
|
||||
handleStyles: HandleStyles;
|
||||
} => {
|
||||
if (!isResizable) {
|
||||
return { containerStyles: {}, handleStyles: {} };
|
||||
}
|
||||
|
||||
const handleWidth = isPinned ? HANDLE_WIDTH_PINNED : HANDLE_WIDTH_UNPINNED;
|
||||
|
||||
// Calculate the positioning offset of the handle hitbox so it is centered over the handle
|
||||
const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${handleWidth}) / -2)`;
|
||||
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
containerStyles: {
|
||||
borderBottomWidth: handleWidth,
|
||||
paddingBottom: HANDLE_PADDING,
|
||||
},
|
||||
handleStyles: {
|
||||
top: {
|
||||
paddingTop: HANDLE_INTERACT_PADDING,
|
||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
||||
bottom: handleOffset,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
return {
|
||||
containerStyles: {
|
||||
borderInlineEndWidth: handleWidth,
|
||||
paddingInlineEnd: HANDLE_PADDING,
|
||||
},
|
||||
handleStyles: {
|
||||
right: {
|
||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
||||
insetInlineEnd: handleOffset,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'bottom') {
|
||||
return {
|
||||
containerStyles: {
|
||||
borderTopWidth: handleWidth,
|
||||
paddingTop: HANDLE_PADDING,
|
||||
},
|
||||
handleStyles: {
|
||||
bottom: {
|
||||
paddingTop: HANDLE_INTERACT_PADDING,
|
||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
||||
top: handleOffset,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
return {
|
||||
containerStyles: {
|
||||
borderInlineStartWidth: handleWidth,
|
||||
paddingInlineStart: HANDLE_PADDING,
|
||||
},
|
||||
handleStyles: {
|
||||
left: {
|
||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
||||
insetInlineStart: handleOffset,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { containerStyles: {}, handleStyles: {} };
|
||||
};
|
||||
|
||||
// Chakra's Slide does not handle langDirection, so we need to do it here
|
||||
export const getSlideDirection = (
|
||||
direction: SlideDirection,
|
||||
langDirection: LangDirection
|
||||
) => {
|
||||
if (['top', 'bottom'].includes(direction)) {
|
||||
return direction;
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
if (langDirection === 'rtl') {
|
||||
return 'right';
|
||||
}
|
||||
return 'left';
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
if (langDirection === 'rtl') {
|
||||
return 'left';
|
||||
}
|
||||
return 'right';
|
||||
}
|
||||
|
||||
return 'left';
|
||||
};
|
||||
|
||||
// Hack to correct the width of panels while pinned and unpinned, due to different padding in pinned vs unpinned
|
||||
export const parseAndPadSize = (size?: number, padding?: number) => {
|
||||
if (!size) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!padding) {
|
||||
return size;
|
||||
}
|
||||
|
||||
return size + padding;
|
||||
};
|
@ -0,0 +1,88 @@
|
||||
import { Box, ChakraProps } from '@chakra-ui/react';
|
||||
import { throttle } from 'lodash';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
const scrollShadowBaseStyles: ChakraProps['sx'] = {
|
||||
position: 'absolute',
|
||||
width: 'full',
|
||||
height: 24,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
};
|
||||
|
||||
type ScrollableProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Scrollable = ({ children }: ScrollableProps) => {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const topShadowRef = useRef<HTMLDivElement>(null);
|
||||
const bottomShadowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateScrollShadowOpacity = throttle(
|
||||
() => {
|
||||
if (
|
||||
!scrollableRef.current ||
|
||||
!topShadowRef.current ||
|
||||
!bottomShadowRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { scrollTop, scrollHeight, offsetHeight } = scrollableRef.current;
|
||||
|
||||
if (scrollTop > 0) {
|
||||
topShadowRef.current.style.opacity = '1';
|
||||
} else {
|
||||
topShadowRef.current.style.opacity = '0';
|
||||
}
|
||||
|
||||
if (scrollTop >= scrollHeight - offsetHeight) {
|
||||
bottomShadowRef.current.style.opacity = '0';
|
||||
} else {
|
||||
bottomShadowRef.current.style.opacity = '1';
|
||||
}
|
||||
},
|
||||
33,
|
||||
{ leading: true }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateScrollShadowOpacity();
|
||||
}, [updateScrollShadowOpacity]);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<Box
|
||||
ref={scrollableRef}
|
||||
position="absolute"
|
||||
w="full"
|
||||
h="full"
|
||||
overflowY="scroll"
|
||||
onScroll={updateScrollShadowOpacity}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<Box
|
||||
ref={bottomShadowRef}
|
||||
sx={{
|
||||
...scrollShadowBaseStyles,
|
||||
bottom: 0,
|
||||
boxShadow:
|
||||
'inset 0 -3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
|
||||
}}
|
||||
></Box>
|
||||
<Box
|
||||
ref={topShadowRef}
|
||||
sx={{
|
||||
...scrollShadowBaseStyles,
|
||||
top: 0,
|
||||
boxShadow:
|
||||
'inset 0 3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scrollable;
|
@ -14,7 +14,7 @@ const workareaSplitViewStyle: ChakraProps['sx'] = {
|
||||
padding: 4,
|
||||
};
|
||||
|
||||
const ImageToImageDisplay = () => {
|
||||
const ImageToImageContent = () => {
|
||||
const initialImage = useAppSelector(
|
||||
(state: RootState) => state.generation.initialImage
|
||||
);
|
||||
@ -47,4 +47,4 @@ const ImageToImageDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageDisplay;
|
||||
export default ImageToImageContent;
|
@ -15,11 +15,11 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
|
||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ImageToImageSettings from './ImageToImageSettings';
|
||||
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
|
||||
import { memo } from 'react';
|
||||
|
||||
export default function ImageToImagePanel() {
|
||||
const ImageToImageParameters = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const imageToImageAccordions = {
|
||||
@ -69,13 +69,13 @@ export default function ImageToImagePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<InvokeOptionsPanel>
|
||||
<Flex flexDir="column" rowGap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} position="relative">
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
<ProcessButtons />
|
||||
<ParametersAccordion accordionInfo={imageToImageAccordions} />
|
||||
</InvokeOptionsPanel>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(ImageToImageParameters);
|
@ -0,0 +1,11 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import ImageToImageContent from './ImageToImageContent';
|
||||
import ImageToImageParameters from './ImageToImageParameters';
|
||||
|
||||
export default function ImageToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea parametersPanelContent={<ImageToImageParameters />}>
|
||||
<ImageToImageContent />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||
|
||||
const TextToImageDisplay = () => {
|
||||
const TextToImageContent = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -18,4 +18,4 @@ const TextToImageDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TextToImageDisplay;
|
||||
export default TextToImageContent;
|
@ -15,10 +15,10 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
|
||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function TextToImagePanel() {
|
||||
const TextToImageParameters = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const textToImageAccordions = {
|
||||
@ -63,13 +63,13 @@ export default function TextToImagePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<InvokeOptionsPanel>
|
||||
<Flex flexDir="column" rowGap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
<ProcessButtons />
|
||||
<ParametersAccordion accordionInfo={textToImageAccordions} />
|
||||
</InvokeOptionsPanel>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(TextToImageParameters);
|
@ -0,0 +1,11 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import TextToImageContent from './TextToImageContent';
|
||||
import TextToImageParameters from './TextToImageParameters';
|
||||
|
||||
export default function TextToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea parametersPanelContent={<TextToImageParameters />}>
|
||||
<TextToImageContent />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -5,12 +5,12 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAICanvas from 'features/canvas/components/IAICanvas';
|
||||
import IAICanvasResizer from 'features/canvas/components/IAICanvasResizer';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import UnifiedCanvasToolbarBeta from './UnifiedCanvasToolbarBeta';
|
||||
import UnifiedCanvasToolSettingsBeta from './UnifiedCanvasToolSettingsBeta';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
|
||||
const selector = createSelector(
|
||||
[canvasSelector],
|
||||
@ -27,17 +27,16 @@ const selector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const UnifiedCanvasDisplayBeta = () => {
|
||||
const UnifiedCanvasContentBeta = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { doesCanvasNeedScaling } = useAppSelector(selector);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
|
||||
const resizeCallback = debounce(() => {
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
}, 250);
|
||||
dispatch(requestCanvasRescale());
|
||||
const resizeCallback = () => {
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCallback);
|
||||
|
||||
@ -70,4 +69,4 @@ const UnifiedCanvasDisplayBeta = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedCanvasDisplayBeta;
|
||||
export default UnifiedCanvasContentBeta;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user