Compare commits

..

16 Commits

Author SHA1 Message Date
psychedelicious
8cde1a2967 fix(ui): staging area left/right hotkeys 2025-07-18 18:51:22 +10:00
psychedelicious
8a6a88742c fix(ui): ensure staging area always has the right state and session association 2025-07-18 18:51:22 +10:00
psychedelicious
12d9862c4e fix(ui): ensure we clean up when session id changes 2025-07-18 18:51:22 +10:00
psychedelicious
a3614b73b5 docs(ui): update StagingAreaApi docstrings 2025-07-18 18:51:22 +10:00
psychedelicious
8f2af4aedd repo: update ignores 2025-07-18 18:51:22 +10:00
psychedelicious
de231d4e0f tests(ui): add test suite for StagingAreaApi 2025-07-18 18:51:22 +10:00
psychedelicious
a5218d1a74 tidy(ui): move staging area components to correct dir 2025-07-18 18:51:22 +10:00
psychedelicious
546cb23071 tidy(ui): move launchpad components to ui dir 2025-07-18 18:51:22 +10:00
psychedelicious
b297892734 chore(ui): rename context2.tsx -> context.tsx 2025-07-18 18:51:22 +10:00
psychedelicious
1e16b92cf6 chore(ui): lint 2025-07-18 18:51:22 +10:00
psychedelicious
019a7ebc66 refactor(ui): move staging area logic out side react
Was running into difficultlies reasoning about the logic and couldn't
write tests because it was all in react.

Moved logic outside react, updated context, make it testable.
2025-07-18 18:51:22 +10:00
psychedelicious
c310ccdbae wip 2025-07-18 18:51:22 +10:00
psychedelicious
b1d181b74f fix(ui): unstyled error boundary 2025-07-18 18:50:06 +10:00
psychedelicious
4ea3ddaf16 fix(ui): use invocation context provider in inspector panel 2025-07-18 18:45:07 +10:00
psychedelicious
4e5eacedce chore(ui): update dockview to latest
Remove extraneous fix now that the disableDnd issue is resolved upstream
2025-07-18 18:44:32 +10:00
psychedelicious
3aa0c500ec chore(ui): bump version to v6.1.0rc2 2025-07-18 18:42:39 +10:00
60 changed files with 560 additions and 3065 deletions

View File

@@ -51,7 +51,6 @@ from invokeai.backend.model_manager.metadata import (
from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata
from invokeai.backend.model_manager.search import ModelSearch
from invokeai.backend.model_manager.taxonomy import ModelRepoVariant, ModelSourceType
from invokeai.backend.model_manager.util.lora_metadata_extractor import apply_lora_metadata
from invokeai.backend.util import InvokeAILogger
from invokeai.backend.util.catch_sigint import catch_sigint
from invokeai.backend.util.devices import TorchDevice
@@ -668,10 +667,6 @@ class ModelInstallService(ModelInstallServiceBase):
info = info or self._probe(model_path, config)
# Apply LoRA metadata if applicable
model_images_path = self.app_config.models_path / "model_images"
apply_lora_metadata(info, model_path.resolve(), model_images_path)
model_path = model_path.resolve()
# Models in the Invoke-managed models dir should use relative paths.

View File

@@ -1,145 +0,0 @@
"""Utility functions for extracting metadata from LoRA model files."""
import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional, Set, Tuple
from PIL import Image
from invokeai.app.util.thumbnails import make_thumbnail
from invokeai.backend.model_manager.config import AnyModelConfig, ModelType
logger = logging.getLogger(__name__)
def extract_lora_metadata(
model_path: Path, model_key: str, model_images_path: Path
) -> Tuple[Optional[str], Optional[Set[str]]]:
"""
Extract metadata for a LoRA model from associated JSON and image files.
Args:
model_path: Path to the LoRA model file
model_key: Unique key for the model
model_images_path: Path to the model images directory
Returns:
Tuple of (description, trigger_phrases)
"""
model_stem = model_path.stem
model_dir = model_path.parent
# Find and process preview image
_process_preview_image(model_stem, model_dir, model_key, model_images_path)
# Extract metadata from JSON
description, trigger_phrases = _extract_json_metadata(model_stem, model_dir)
return description, trigger_phrases
def _process_preview_image(model_stem: str, model_dir: Path, model_key: str, model_images_path: Path) -> bool:
"""Find and process a preview image for the model, saving it to the model images store."""
image_extensions = [".png", ".jpg", ".jpeg", ".webp"]
for ext in image_extensions:
image_path = model_dir / f"{model_stem}{ext}"
if image_path.exists():
try:
# Open the image
with Image.open(image_path) as img:
# Create thumbnail and save to model images directory
thumbnail = make_thumbnail(img, 256)
thumbnail_path = model_images_path / f"{model_key}.webp"
thumbnail.save(thumbnail_path, format="webp")
logger.info(f"Processed preview image {image_path.name} for model {model_key}")
return True
except Exception as e:
logger.warning(f"Failed to process preview image {image_path.name}: {e}")
return False
return False
def _extract_json_metadata(model_stem: str, model_dir: Path) -> Tuple[Optional[str], Optional[Set[str]]]:
"""Extract metadata from a JSON file with the same name as the model."""
json_path = model_dir / f"{model_stem}.json"
if not json_path.exists():
return None, None
try:
with open(json_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
# Extract description
description = _build_description(metadata)
# Extract trigger phrases
trigger_phrases = _extract_trigger_phrases(metadata)
if description or trigger_phrases:
logger.info(f"Applied metadata from {json_path.name}")
return description, trigger_phrases
except (json.JSONDecodeError, IOError, Exception) as e:
logger.warning(f"Failed to read metadata from {json_path}: {e}")
return None, None
def _build_description(metadata: Dict[str, Any]) -> Optional[str]:
"""Build a description from metadata fields."""
description_parts = []
if description := metadata.get("description"):
description_parts.append(str(description).strip())
if notes := metadata.get("notes"):
description_parts.append(str(notes).strip())
return " | ".join(description_parts) if description_parts else None
def _extract_trigger_phrases(metadata: Dict[str, Any]) -> Optional[Set[str]]:
"""Extract trigger phrases from metadata."""
if not (activation_text := metadata.get("activation text")):
return None
activation_text = str(activation_text).strip()
if not activation_text:
return None
# Split on commas and clean up each phrase
phrases = [phrase.strip() for phrase in activation_text.split(",") if phrase.strip()]
return set(phrases) if phrases else None
def apply_lora_metadata(info: AnyModelConfig, model_path: Path, model_images_path: Path) -> None:
"""
Apply extracted metadata to a LoRA model configuration.
Args:
info: The model configuration to update
model_path: Path to the LoRA model file
model_images_path: Path to the model images directory
"""
# Only process LoRA models
if info.type != ModelType.LoRA:
return
# Extract and apply metadata
description, trigger_phrases = extract_lora_metadata(model_path, info.key, model_images_path)
# We don't set cover_image path in the config anymore since images are stored
# separately in the model images store by model key
if description:
info.description = description
if trigger_phrases:
info.trigger_phrases = trigger_phrases

View File

@@ -711,8 +711,7 @@
"gaussianBlur": "Gaußsche Unschärfe",
"sendToUpscale": "An Hochskalieren senden",
"useCpuNoise": "CPU-Rauschen verwenden",
"sendToCanvas": "An Leinwand senden",
"disabledNoRasterContent": "Deaktiviert (kein Rasterinhalt)"
"sendToCanvas": "An Leinwand senden"
},
"settings": {
"displayInProgress": "Zwischenbilder anzeigen",
@@ -790,10 +789,7 @@
"pasteSuccess": "Eingefügt in {{destination}}",
"pasteFailed": "Einfügen fehlgeschlagen",
"unableToCopy": "Kopieren nicht möglich",
"unableToCopyDesc_theseSteps": "diese Schritte",
"noRasterLayers": "Keine Rasterebenen gefunden",
"noActiveRasterLayers": "Keine aktiven Rasterebenen",
"noVisibleRasterLayers": "Keine sichtbaren Rasterebenen"
"unableToCopyDesc_theseSteps": "diese Schritte"
},
"accessibility": {
"uploadImage": "Bild hochladen",
@@ -851,10 +847,7 @@
"assetsWithCount_one": "{{count}} in der Sammlung",
"assetsWithCount_other": "{{count}} in der Sammlung",
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden. Die Auswahl von \"Nur Ordner löschen\" verschiebt Bilder in einen unkategorisierten Zustand.",
"updateBoardError": "Fehler beim Aktualisieren des Ordners",
"uncategorizedImages": "Nicht kategorisierte Bilder",
"deleteAllUncategorizedImages": "Alle nicht kategorisierten Bilder löschen",
"deletedImagesCannotBeRestored": "Gelöschte Bilder können nicht wiederhergestellt werden."
"updateBoardError": "Fehler beim Aktualisieren des Ordners"
},
"queue": {
"status": "Status",
@@ -1201,9 +1194,6 @@
"Die Kantengröße des Kohärenzdurchlaufs."
],
"heading": "Kantengröße"
},
"rasterLayer": {
"heading": "Rasterebene"
}
},
"invocationCache": {
@@ -1441,10 +1431,7 @@
"autoLayout": "Auto Layout",
"copyShareLink": "Teilen-Link kopieren",
"download": "Herunterladen",
"convertGraph": "Graph konvertieren",
"filterByTags": "Nach Tags filtern",
"yourWorkflows": "Ihre Arbeitsabläufe",
"recentlyOpened": "Kürzlich geöffnet"
"convertGraph": "Graph konvertieren"
},
"sdxl": {
"concatPromptStyle": "Verknüpfen von Prompt & Stil",
@@ -1457,15 +1444,7 @@
"prompt": {
"noMatchingTriggers": "Keine passenden Trigger",
"addPromptTrigger": "Prompt-Trigger hinzufügen",
"compatibleEmbeddings": "Kompatible Einbettungen",
"replace": "Ersetzen",
"insert": "Einfügen",
"discard": "Verwerfen",
"generateFromImage": "Prompt aus Bild generieren",
"expandCurrentPrompt": "Aktuelle Prompt erweitern",
"uploadImageForPromptGeneration": "Bild zur Prompt-Generierung hochladen",
"expandingPrompt": "Prompt wird erweitert...",
"resultTitle": "Prompt-Erweiterung abgeschlossen"
"compatibleEmbeddings": "Kompatible Einbettungen"
},
"ui": {
"tabs": {
@@ -1594,30 +1573,30 @@
"newGlobalReferenceImage": "Neues globales Referenzbild",
"newRegionalReferenceImage": "Neues regionales Referenzbild",
"newControlLayer": "Neue Kontroll-Ebene",
"newRasterLayer": "Neue Rasterebene"
"newRasterLayer": "Neue Raster-Ebene"
},
"rectangle": "Rechteck",
"saveCanvasToGallery": "Leinwand in Galerie speichern",
"newRasterLayerError": "Problem beim Erstellen einer Rasterebene",
"newRasterLayerError": "Problem beim Erstellen einer Raster-Ebene",
"saveLayerToAssets": "Ebene in Galerie speichern",
"deleteReferenceImage": "Referenzbild löschen",
"referenceImage": "Referenzbild",
"opacity": "Opazität",
"removeBookmark": "Lesezeichen entfernen",
"rasterLayer": "Rasterebene",
"rasterLayers_withCount_visible": "Rasterebenen ({{count}})",
"rasterLayer": "Raster-Ebene",
"rasterLayers_withCount_visible": "Raster-Ebenen ({{count}})",
"controlLayers_withCount_visible": "Kontroll-Ebenen ({{count}})",
"deleteSelected": "Ausgewählte löschen",
"newRegionalReferenceImageError": "Problem beim Erstellen eines regionalen Referenzbilds",
"newControlLayerOk": "Kontroll-Ebene erstellt",
"newControlLayerError": "Problem beim Erstellen einer Kontroll-Ebene",
"newRasterLayerOk": "Rasterebene erstellt",
"newRasterLayerOk": "Raster-Layer erstellt",
"moveToFront": "Nach vorne bringen",
"copyToClipboard": "In die Zwischenablage kopieren",
"controlLayers_withCount_hidden": "Kontroll-Ebenen ({{count}} ausgeblendet)",
"clearCaches": "Cache leeren",
"controlLayer": "Kontroll-Ebene",
"rasterLayers_withCount_hidden": "Rasterebenen ({{count}} ausgeblendet)",
"rasterLayers_withCount_hidden": "Raster-Ebenen ({{count}} ausgeblendet)",
"transparency": "Transparenz",
"canvas": "Leinwand",
"global": "Global",
@@ -1703,14 +1682,7 @@
"filterType": "Filtertyp",
"filter": "Filter"
},
"bookmark": "Lesezeichen für Schnell-Umschalten",
"asRasterLayer": "Als $t(controlLayers.rasterLayer)",
"asRasterLayerResize": "Als $t(controlLayers.rasterLayer) (Größe anpassen)",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"rasterLayer_withCount_other": "Rasterebenen",
"newRasterLayer": "Neue $t(controlLayers.rasterLayer)",
"showNonRasterLayers": "Nicht-Rasterebenen anzeigen (Umschalt+H)",
"hideNonRasterLayers": "Nicht-Rasterebenen ausblenden (Umschalt+H)"
"bookmark": "Lesezeichen für Schnell-Umschalten"
},
"upsell": {
"shareAccess": "Zugang teilen",

View File

@@ -253,7 +253,6 @@
"cancel": "Cancel",
"cancelAllExceptCurrentQueueItemAlertDialog": "Canceling all queue items except the current one will stop pending items but allow the in-progress one to finish.",
"cancelAllExceptCurrentQueueItemAlertDialog2": "Are you sure you want to cancel all pending queue items?",
"cancelAllExceptCurrent": "Cancel All Except Current",
"cancelAllExceptCurrentTooltip": "Cancel All Except Current Item",
"cancelTooltip": "Cancel Current Item",
"cancelSucceeded": "Item Canceled",
@@ -274,7 +273,7 @@
"retryItem": "Retry Item",
"cancelBatchSucceeded": "Batch Canceled",
"cancelBatchFailed": "Problem Canceling Batch",
"clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely. Pending filters will be canceled and the Canvas Staging Area will be reset.",
"clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely. Pending filters will be canceled.",
"clearQueueAlertDialog2": "Are you sure you want to clear the queue?",
"current": "Current",
"next": "Next",
@@ -471,11 +470,6 @@
"togglePanels": {
"title": "Toggle Panels",
"desc": "Show or hide both left and right panels at once."
},
"selectGenerateTab": {
"title": "Select the Generate Tab",
"desc": "Selects the Generate tab.",
"key": "1"
}
},
"canvas": {
@@ -580,10 +574,6 @@
"title": "Transform",
"desc": "Transform the selected layer."
},
"invertMask": {
"title": "Invert Mask",
"desc": "Invert the selected inpaint mask, creating a new mask with opposite transparency."
},
"applyFilter": {
"title": "Apply Filter",
"desc": "Apply the pending filter to the selected layer."
@@ -609,20 +599,6 @@
"toggleNonRasterLayers": {
"title": "Toggle Non-Raster Layers",
"desc": "Show or hide all non-raster layer categories (Control Layers, Inpaint Masks, Regional Guidance)."
},
"fitBboxToMasks": {
"title": "Fit Bbox To Masks",
"desc": "Automatically adjust the generation bounding box to fit visible inpaint masks"
},
"applySegmentAnything": {
"title": "Apply Segment Anything",
"desc": "Apply the current Segment Anything mask.",
"key": "enter"
},
"cancelSegmentAnything": {
"title": "Cancel Segment Anything",
"desc": "Cancel the current Segment Anything operation.",
"key": "esc"
}
},
"workflows": {
@@ -752,10 +728,6 @@
"deleteSelection": {
"title": "Delete",
"desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned."
},
"starImage": {
"title": "Star/Unstar Image",
"desc": "Star or unstar the selected image."
}
}
},
@@ -1153,23 +1125,7 @@
"addItem": "Add Item",
"generateValues": "Generate Values",
"floatRangeGenerator": "Float Range Generator",
"integerRangeGenerator": "Integer Range Generator",
"layout": {
"autoLayout": "Auto Layout",
"layeringStrategy": "Layering Strategy",
"networkSimplex": "Network Simplex",
"longestPath": "Longest Path",
"nodeSpacing": "Node Spacing",
"layerSpacing": "Layer Spacing",
"layoutDirection": "Layout Direction",
"layoutDirectionRight": "Right",
"layoutDirectionDown": "Down",
"alignment": "Node Alignment",
"alignmentUL": "Top Left",
"alignmentDL": "Bottom Left",
"alignmentUR": "Top Right",
"alignmentDR": "Bottom Right"
}
"integerRangeGenerator": "Integer Range Generator"
},
"parameters": {
"aspect": "Aspect",
@@ -1451,15 +1407,7 @@
"sentToUpscale": "Sent to Upscale",
"promptGenerationStarted": "Prompt generation started",
"uploadAndPromptGenerationFailed": "Failed to upload image and generate prompt",
"promptExpansionFailed": "We ran into an issue. Please try prompt expansion again.",
"maskInverted": "Mask Inverted",
"maskInvertFailed": "Failed to Invert Mask",
"noVisibleMasks": "No Visible Masks",
"noVisibleMasksDesc": "Create or enable at least one inpaint mask to invert",
"noInpaintMaskSelected": "No Inpaint Mask Selected",
"noInpaintMaskSelectedDesc": "Select an inpaint mask to invert",
"invalidBbox": "Invalid Bounding Box",
"invalidBboxDesc": "The bounding box has no valid dimensions"
"promptExpansionFailed": "We ran into an issue. Please try prompt expansion again."
},
"popovers": {
"clipSkip": {
@@ -1992,7 +1940,6 @@
"canvas": "Canvas",
"bookmark": "Bookmark for Quick Switch",
"fitBboxToLayers": "Fit Bbox To Layers",
"fitBboxToMasks": "Fit Bbox To Masks",
"removeBookmark": "Remove Bookmark",
"saveCanvasToGallery": "Save Canvas to Gallery",
"saveBboxToGallery": "Save Bbox to Gallery",
@@ -2057,7 +2004,6 @@
"rasterLayer": "Raster Layer",
"controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask",
"invertMask": "Invert Mask",
"regionalGuidance": "Regional Guidance",
"referenceImageRegional": "Reference Image (Regional)",
"referenceImageGlobal": "Reference Image (Global)",
@@ -2631,10 +2577,9 @@
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"New setting to send all Canvas generations directly to the Gallery.",
"New Invert Mask (Shift+V) and Fit BBox to Mask (Shift+B) capabilities.",
"Expanded support for Model Thumbnails and configurations.",
"Various other quality of life updates and fixes"
"Generate images faster with new Launchpads and a simplified Generate tab.",
"Edit with prompts using Flux Kontext Dev.",
"Export to PSD, bulk-hide overlays, organize models & images — all in a reimagined interface built for control."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",

View File

@@ -2375,8 +2375,65 @@
},
"supportVideos": {
"watch": "Regarder",
"videos": {
"upscaling": {
"description": "Comment améliorer la résolution des images avec les outils d'Invoke pour les agrandir.",
"title": "Upscaling"
},
"howDoIGenerateAndSaveToTheGallery": {
"description": "Étapes pour générer et enregistrer des images dans la galerie.",
"title": "Comment générer et enregistrer dans la galerie?"
},
"usingControlLayersAndReferenceGuides": {
"title": "Utilisation des couche de contrôle et des guides de référence",
"description": "Apprenez à guider la création de vos images avec des couche de contrôle et des images de référence."
},
"exploringAIModelsAndConceptAdapters": {
"description": "Plongez dans les modèles d'IA et découvrez comment utiliser les adaptateurs de concepts pour un contrôle créatif.",
"title": "Exploration des modèles d'IA et des adaptateurs de concepts"
},
"howDoIUseControlNetsAndControlLayers": {
"title": "Comment utiliser les réseaux de contrôle et les couches de contrôle?",
"description": "Apprenez à appliquer des couches de contrôle et des ControlNets à vos images."
},
"creatingAndComposingOnInvokesControlCanvas": {
"description": "Apprenez à composer des images en utilisant le canvas de contrôle d'Invoke.",
"title": "Créer et composer sur le canvas de contrôle d'Invoke"
},
"howDoIEditOnTheCanvas": {
"title": "Comment puis-je modifier sur la toile?",
"description": "Guide pour éditer des images directement sur la toile."
},
"howDoIDoImageToImageTransformation": {
"title": "Comment effectuer une transformation d'image à image?",
"description": "Tutoriel sur la réalisation de transformations d'image à image dans Invoke."
},
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
"title": "Comment utiliser les IP Adapters globaux et les images de référence?",
"description": "Introduction à l'ajout d'images de référence et IP Adapters globaux."
},
"howDoIUseInpaintMasks": {
"title": "Comment utiliser les masques d'inpainting?",
"description": "Comment appliquer des masques de retourche pour la correction et la variation d'image."
},
"creatingYourFirstImage": {
"title": "Créer votre première image",
"description": "Introduction à la création d'une image à partir de zéro en utilisant les outils d'Invoke."
},
"understandingImageToImageAndDenoising": {
"title": "Comprendre l'Image-à-Image et le Débruitage",
"description": "Aperçu des transformations d'image à image et du débruitage dans Invoke."
},
"howDoIOutpaint": {
"title": "Comment effectuer un outpainting?",
"description": "Guide pour l'extension au-delà des bordures de l'image originale."
}
},
"gettingStarted": "Commencer",
"supportVideos": "Vidéos d'assistance"
"studioSessionsDesc1": "Consultez le <StudioSessionsPlaylistLink /> pour des approfondissements sur Invoke.",
"studioSessionsDesc2": "Rejoignez notre <DiscordLink /> pour participer aux sessions en direct et poser vos questions. Les sessions sont ajoutée dans la playlist la semaine suivante.",
"supportVideos": "Vidéos d'assistance",
"controlCanvas": "Contrôler la toile"
},
"modelCache": {
"clear": "Effacer le cache du modèle",

View File

@@ -152,7 +152,7 @@
"image": "immagine",
"drop": "Rilascia",
"unstarImage": "Rimuovi contrassegno immagine",
"dropOrUpload": "Rilascia o carica",
"dropOrUpload": "$t(gallery.drop) o carica",
"starImage": "Contrassegna l'immagine",
"dropToUpload": "$t(gallery.drop) per aggiornare",
"bulkDownloadRequested": "Preparazione del download",
@@ -197,8 +197,7 @@
"boardsSettings": "Impostazioni Bacheche",
"imagesSettings": "Impostazioni Immagini Galleria",
"assets": "Risorse",
"images": "Immagini",
"useForPromptGeneration": "Usa per generare il prompt"
"images": "Immagini"
},
"hotkeys": {
"searchHotkeys": "Cerca tasti di scelta rapida",
@@ -254,16 +253,12 @@
"desc": "Attiva/disattiva il pannello destro."
},
"resetPanelLayout": {
"title": "Ripristina lo schema del pannello",
"desc": "Ripristina le dimensioni e lo schema predefiniti dei pannelli sinistro e destro."
"title": "Ripristina il layout del pannello",
"desc": "Ripristina le dimensioni e il layout predefiniti dei pannelli sinistro e destro."
},
"togglePanels": {
"title": "Attiva/disattiva i pannelli",
"desc": "Mostra o nascondi contemporaneamente i pannelli sinistro e destro."
},
"selectGenerateTab": {
"title": "Seleziona la scheda Genera",
"desc": "Seleziona la scheda Genera."
}
},
"hotkeys": "Tasti di scelta rapida",
@@ -384,32 +379,6 @@
"applyTransform": {
"title": "Applica trasformazione",
"desc": "Applica la trasformazione in sospeso al livello selezionato."
},
"toggleNonRasterLayers": {
"desc": "Mostra o nascondi tutte le categorie di livelli non raster (Livelli di controllo, Maschere di Inpaint, Guida regionale).",
"title": "Attiva/disattiva livelli non raster"
},
"settings": {
"behavior": "Comportamento",
"display": "Mostra",
"grid": "Griglia"
},
"invertMask": {
"title": "Inverti maschera",
"desc": "Inverte la maschera di inpaint selezionata, creando una nuova maschera con trasparenza opposta."
},
"fitBboxToMasks": {
"title": "Adatta il riquadro di delimitazione alle maschere",
"desc": "Regola automaticamente il riquadro di delimitazione della generazione per adattarlo alle maschere di inpaint visibili"
},
"applySegmentAnything": {
"title": "Applica Segment Anything",
"desc": "Applica la maschera Segment Anything corrente.",
"key": "invio"
},
"cancelSegmentAnything": {
"title": "Annulla Segment Anything",
"desc": "Annulla l'operazione Segment Anything corrente."
}
},
"workflows": {
@@ -539,10 +508,6 @@
"galleryNavUpAlt": {
"desc": "Uguale a Naviga verso l'alto, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
"title": "Naviga verso l'alto (Confronta immagine)"
},
"starImage": {
"desc": "Aggiungi/Rimuovi contrassegno all'immagine selezionata.",
"title": "Aggiungi / Rimuovi contrassegno immagine"
}
}
},
@@ -658,7 +623,7 @@
"installingXModels_one": "Installazione di {{count}} modello",
"installingXModels_many": "Installazione di {{count}} modelli",
"installingXModels_other": "Installazione di {{count}} modelli",
"includesNModels": "Include {{n}} modelli e le loro dipendenze.",
"includesNModels": "Include {{n}} modelli e le loro dipendenze",
"starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato.",
"noDefaultSettings": "Nessuna impostazione predefinita configurata per questo modello. Visita Gestione Modelli per aggiungere impostazioni predefinite.",
"defaultSettingsOutOfSync": "Alcune impostazioni non corrispondono a quelle predefinite del modello:",
@@ -691,27 +656,7 @@
"manageModels": "Gestione modelli",
"hfTokenReset": "Ripristino del gettone HF",
"relatedModels": "Modelli correlati",
"showOnlyRelatedModels": "Correlati",
"installedModelsCount": "{{installed}} di {{total}} modelli installati.",
"allNModelsInstalled": "Tutti i {{count}} modelli installati",
"nToInstall": "{{count}} da installare",
"nAlreadyInstalled": "{{count}} già installati",
"bundleAlreadyInstalled": "Pacchetto già installato",
"bundleAlreadyInstalledDesc": "Tutti i modelli nel pacchetto {{bundleName}} sono già installati.",
"launchpad": {
"description": "Per utilizzare la maggior parte delle funzionalità della piattaforma, Invoke richiede l'installazione di modelli. Scegli tra le opzioni di installazione manuale o esplora i modelli di avvio selezionati.",
"manualInstall": "Installazione manuale",
"urlDescription": "Installa i modelli da un URL o da un percorso file locale. Perfetto per modelli specifici che desideri aggiungere.",
"huggingFaceDescription": "Esplora e installa i modelli direttamente dai repository di HuggingFace.",
"scanFolderDescription": "Esegui la scansione di una cartella locale per rilevare e installare automaticamente i modelli.",
"recommendedModels": "Modelli consigliati",
"exploreStarter": "Oppure sfoglia tutti i modelli iniziali disponibili",
"welcome": "Benvenuti in Gestione Modelli",
"quickStart": "Pacchetti di avvio rapido",
"bundleDescription": "Ogni pacchetto include modelli essenziali per ogni famiglia di modelli e modelli base selezionati per iniziare.",
"browseAll": "Oppure scopri tutti i modelli disponibili:"
},
"launchpadTab": "Rampa di lancio"
"showOnlyRelatedModels": "Correlati"
},
"parameters": {
"images": "Immagini",
@@ -797,10 +742,7 @@
"modelIncompatibleBboxHeight": "L'altezza del riquadro è {{height}} ma {{model}} richiede multipli di {{multiple}}",
"modelIncompatibleScaledBboxWidth": "La larghezza scalata del riquadro è {{width}} ma {{model}} richiede multipli di {{multiple}}",
"modelIncompatibleScaledBboxHeight": "L'altezza scalata del riquadro è {{height}} ma {{model}} richiede multipli di {{multiple}}",
"modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Accedi alle impostazioni del tuo account per effettuare l'upgrade.",
"fluxKontextMultipleReferenceImages": "È possibile utilizzare solo 1 immagine di riferimento alla volta con Flux Kontext",
"promptExpansionResultPending": "Accetta o ignora il risultato dell'espansione del prompt",
"promptExpansionPending": "Espansione del prompt in corso"
"modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Accedi alle impostazioni del tuo account per effettuare l'upgrade."
},
"useCpuNoise": "Usa la CPU per generare rumore",
"iterations": "Iterazioni",
@@ -942,34 +884,7 @@
"problemUnpublishingWorkflowDescription": "Si è verificato un problema durante l'annullamento della pubblicazione del flusso di lavoro. Riprova.",
"workflowUnpublished": "Flusso di lavoro non pubblicato",
"chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o supporta solo la conversione da testo a immagine e da immagine a immagine. Utilizza altri modelli per le attività di Inpainting e Outpainting.",
"imagenIncompatibleGenerationMode": "Google {{model}} supporta solo la generazione da testo a immagine. Utilizza altri modelli per le attività di conversione da immagine a immagine, inpainting e outpainting.",
"noRasterLayers": "Nessun livello raster trovato",
"noRasterLayersDesc": "Crea almeno un livello raster da esportare in PSD",
"noActiveRasterLayers": "Nessun livello raster attivo",
"noActiveRasterLayersDesc": "Abilitare almeno un livello raster da esportare in PSD",
"noVisibleRasterLayers": "Nessun livello raster visibile",
"noVisibleRasterLayersDesc": "Abilitare almeno un livello raster da esportare in PSD",
"invalidCanvasDimensions": "Dimensioni della tela non valide",
"canvasTooLarge": "Tela troppo grande",
"canvasTooLargeDesc": "Le dimensioni della tela superano le dimensioni massime consentite per l'esportazione in formato PSD. Riduci la larghezza e l'altezza totali della tela e riprova.",
"failedToProcessLayers": "Impossibile elaborare i livelli",
"psdExportSuccess": "Esportazione PSD completata",
"psdExportSuccessDesc": "Esportazione riuscita di {{count}} livelli nel file PSD",
"problemExportingPSD": "Problema durante l'esportazione PSD",
"noValidLayerAdapters": "Nessun adattatore di livello valido trovato",
"fluxKontextIncompatibleGenerationMode": "FLUX Kontext non supporta la generazione di immagini posizionate sulla tela. Riprova utilizzando la sezione Immagine di riferimento e disattiva tutti i livelli raster.",
"canvasManagerNotAvailable": "Gestione tela non disponibile",
"promptExpansionFailed": "Abbiamo riscontrato un problema. Riprova a eseguire l'espansione del prompt.",
"uploadAndPromptGenerationFailed": "Impossibile caricare l'immagine e generare il prompt",
"promptGenerationStarted": "Generazione del prompt avviata",
"invalidBboxDesc": "Il riquadro di delimitazione non ha dimensioni valide",
"invalidBbox": "Riquadro di delimitazione non valido",
"noInpaintMaskSelectedDesc": "Seleziona una maschera di inpaint da invertire",
"noInpaintMaskSelected": "Nessuna maschera di inpaint selezionata",
"noVisibleMasksDesc": "Crea o abilita almeno una maschera inpaint da invertire",
"noVisibleMasks": "Nessuna maschera visibile",
"maskInvertFailed": "Impossibile invertire la maschera",
"maskInverted": "Maschera invertita"
"imagenIncompatibleGenerationMode": "Google {{model}} supporta solo la generazione da testo a immagine. Utilizza altri modelli per le attività di conversione da immagine a immagine, inpainting e outpainting."
},
"accessibility": {
"invokeProgressBar": "Barra di avanzamento generazione",
@@ -1164,22 +1079,7 @@
"missingField_withName": "Campo \"{{name}}\" mancante",
"unknownFieldEditWorkflowToFix_withName": "Il flusso di lavoro contiene un campo \"{{name}}\" sconosciuto .\nModifica il flusso di lavoro per risolvere il problema.",
"unexpectedField_withName": "Campo \"{{name}}\" inaspettato",
"missingSourceOrTargetHandle": "Identificatore del nodo sorgente o di destinazione mancante",
"layout": {
"alignmentDR": "In basso a destra",
"autoLayout": "Schema automatico",
"nodeSpacing": "Spaziatura nodi",
"layerSpacing": "Spaziatura livelli",
"layeringStrategy": "Strategia livelli",
"longestPath": "Percorso più lungo",
"layoutDirection": "Direzione schema",
"layoutDirectionRight": "Orizzontale",
"layoutDirectionDown": "Verticale",
"alignment": "Allineamento nodi",
"alignmentUL": "In alto a sinistra",
"alignmentDL": "In basso a sinistra",
"alignmentUR": "In alto a destra"
}
"missingSourceOrTargetHandle": "Identificatore del nodo sorgente o di destinazione mancante"
},
"boards": {
"autoAddBoard": "Aggiungi automaticamente bacheca",
@@ -1256,7 +1156,7 @@
"batchQueuedDesc_other": "Aggiunte {{count}} sessioni a {{direction}} della coda",
"graphQueued": "Grafico in coda",
"batch": "Lotto",
"clearQueueAlertDialog": "La cancellazione della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda. I filtri in sospeso verranno annullati e l'area di lavoro della Tela verrà reimpostata.",
"clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda. I filtri in sospeso verranno annullati.",
"pending": "In attesa",
"completedIn": "Completato in",
"resumeFailed": "Problema nel riavvio dell'elaborazione",
@@ -1312,8 +1212,7 @@
"retrySucceeded": "Elemento rieseguito",
"retryItem": "Riesegui elemento",
"retryFailed": "Problema riesecuzione elemento",
"credits": "Crediti",
"cancelAllExceptCurrent": "Annulla tutto tranne quello corrente"
"credits": "Crediti"
},
"models": {
"noMatchingModels": "Nessun modello corrispondente",
@@ -1326,8 +1225,7 @@
"addLora": "Aggiungi LoRA",
"defaultVAE": "VAE predefinito",
"concepts": "Concetti",
"lora": "LoRA",
"noCompatibleLoRAs": "Nessun LoRA compatibile"
"lora": "LoRA"
},
"invocationCache": {
"disable": "Disabilita",
@@ -1728,7 +1626,7 @@
"structure": {
"heading": "Struttura",
"paragraphs": [
"La struttura determina quanto l'immagine finale rispecchierà il layout dell'originale. Un valore struttura basso permette cambiamenti significativi, mentre un valore struttura alto conserva la composizione e lo schema originali."
"La struttura determina quanto l'immagine finale rispecchierà il layout dell'originale. Una struttura bassa permette cambiamenti significativi, mentre una struttura alta conserva la composizione e il layout originali."
]
},
"fluxDevLicense": {
@@ -1785,20 +1683,6 @@
"paragraphs": [
"Controlla quale area viene modificata, in base all'intensità di riduzione del rumore."
]
},
"tileSize": {
"heading": "Dimensione riquadro",
"paragraphs": [
"Controlla la dimensione dei riquadri utilizzati durante il processo di ampliamento. Riquadri più grandi consumano più memoria, ma possono produrre risultati migliori.",
"I modelli SD1.5 hanno un valore predefinito di 768, mentre i modelli SDXL hanno un valore predefinito di 1024. Ridurre le dimensioni dei riquadri in caso di problemi di memoria."
]
},
"tileOverlap": {
"heading": "Sovrapposizione riquadri",
"paragraphs": [
"Controlla la sovrapposizione tra riquadri adiacenti durante l'ampliamento. Valori di sovrapposizione più elevati aiutano a ridurre le giunzioni visibili tra i riquadri, ma consuma più memoria.",
"Il valore predefinito di 128 è adatto alla maggior parte dei casi, ma è possibile modificarlo in base alle proprie esigenze specifiche e ai limiti di memoria."
]
}
},
"sdxl": {
@@ -1846,7 +1730,7 @@
"parameterSet": "Parametro {{parameter}} impostato",
"parsingFailed": "Analisi non riuscita",
"recallParameter": "Richiama {{label}}",
"canvasV2Metadata": "Livelli Tela",
"canvasV2Metadata": "Tela",
"guidance": "Guida",
"seamlessXAxis": "Asse X senza giunte",
"seamlessYAxis": "Asse Y senza giunte",
@@ -1894,7 +1778,7 @@
"opened": "Aperto",
"convertGraph": "Converti grafico",
"loadWorkflow": "$t(common.load) Flusso di lavoro",
"autoLayout": "Schema automatico",
"autoLayout": "Disposizione automatica",
"loadFromGraph": "Carica il flusso di lavoro dal grafico",
"userWorkflows": "Flussi di lavoro utente",
"projectWorkflows": "Flussi di lavoro del progetto",
@@ -2017,16 +1901,7 @@
"prompt": {
"compatibleEmbeddings": "Incorporamenti compatibili",
"addPromptTrigger": "Aggiungi Trigger nel prompt",
"noMatchingTriggers": "Nessun Trigger corrispondente",
"discard": "Scarta",
"insert": "Inserisci",
"replace": "Sostituisci",
"resultSubtitle": "Scegli come gestire il prompt espanso:",
"resultTitle": "Espansione del prompt completata",
"expandingPrompt": "Espansione del prompt...",
"uploadImageForPromptGeneration": "Carica l'immagine per la generazione del prompt",
"expandCurrentPrompt": "Espandi il prompt corrente",
"generateFromImage": "Genera prompt dall'immagine"
"noMatchingTriggers": "Nessun Trigger corrispondente"
},
"controlLayers": {
"addLayer": "Aggiungi Livello",
@@ -2337,11 +2212,7 @@
"label": "Preserva la regione mascherata"
},
"isolatedLayerPreview": "Anteprima livello isolato",
"isolatedLayerPreviewDesc": "Se visualizzare solo questo livello quando si eseguono operazioni come il filtraggio o la trasformazione.",
"saveAllImagesToGallery": {
"alert": "Invia le nuove generazioni alla Galleria, bypassando la Tela",
"label": "Invia le nuove generazioni alla Galleria"
}
"isolatedLayerPreviewDesc": "Se visualizzare solo questo livello quando si eseguono operazioni come il filtraggio o la trasformazione."
},
"transform": {
"reset": "Reimposta",
@@ -2391,8 +2262,7 @@
"newRegionalGuidance": "Nuova Guida Regionale",
"copyToClipboard": "Copia negli appunti",
"copyCanvasToClipboard": "Copia la tela negli appunti",
"copyBboxToClipboard": "Copia il riquadro di delimitazione negli appunti",
"newResizedControlLayer": "Nuovo livello di controllo ridimensionato"
"copyBboxToClipboard": "Copia il riquadro di delimitazione negli appunti"
},
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine",
"copyRasterLayerTo": "Copia $t(controlLayers.rasterLayer) in",
@@ -2429,10 +2299,10 @@
"replaceCurrent": "Sostituisci corrente",
"mergeDown": "Unire in basso",
"mergingLayers": "Unione dei livelli",
"controlLayerEmptyState": "<UploadButton>Carica un'immagine</UploadButton>, trascina un'immagine dalla galleria su questo livello, <PullBboxButton>trascina il riquadro di delimitazione in questo livello</PullBboxButton> oppure disegna sulla tela per iniziare.",
"controlLayerEmptyState": "<UploadButton>Carica un'immagine</UploadButton>, trascina un'immagine dalla <GalleryButton>galleria</GalleryButton> su questo livello, <PullBboxButton>trascina il riquadro di delimitazione in questo livello</PullBboxButton> oppure disegna sulla tela per iniziare.",
"useImage": "Usa immagine",
"resetGenerationSettings": "Ripristina impostazioni di generazione",
"referenceImageEmptyState": "Per iniziare, <UploadButton>carica un'immagine</UploadButton> oppure trascina un'immagine dalla galleria su questa Immagine di riferimento.",
"referenceImageEmptyState": "Per iniziare, <UploadButton>carica un'immagine</UploadButton>, trascina un'immagine dalla <GalleryButton>galleria</GalleryButton>, oppure <PullBboxButton>trascina il riquadro di delimitazione in questo livello</PullBboxButton> su questo livello.",
"asRasterLayer": "Come $t(controlLayers.rasterLayer)",
"asRasterLayerResize": "Come $t(controlLayers.rasterLayer) (Ridimensiona)",
"asControlLayer": "Come $t(controlLayers.controlLayer)",
@@ -2482,20 +2352,7 @@
"denoiseLimit": "Limite di riduzione del rumore",
"addImageNoise": "Aggiungi $t(controlLayers.imageNoise)",
"addDenoiseLimit": "Aggiungi $t(controlLayers.denoiseLimit)",
"imageNoise": "Rumore dell'immagine",
"exportCanvasToPSD": "Esporta la tela in PSD",
"ruleOfThirds": "Mostra la regola dei terzi",
"showNonRasterLayers": "Mostra livelli non raster (Shift+H)",
"hideNonRasterLayers": "Nascondi livelli non raster (Shift+H)",
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Carica un'immagine</UploadButton>, trascina un'immagine dalla galleria su questa immagine di riferimento o <PullBboxButton>trascina il riquadro di delimitazione in questa immagine di riferimento</PullBboxButton> per iniziare.",
"uploadOrDragAnImage": "Trascina un'immagine dalla galleria o <UploadButton>carica un'immagine</UploadButton>.",
"autoSwitch": {
"switchOnStart": "All'inizio",
"switchOnFinish": "Alla fine",
"off": "Spento"
},
"invertMask": "Inverti maschera",
"fitBboxToMasks": "Adatta il riquadro di delimitazione alle maschere"
"imageNoise": "Rumore dell'immagine"
},
"ui": {
"tabs": {
@@ -2509,55 +2366,6 @@
"upscaling": "Amplia",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
"gallery": "Galleria"
},
"launchpad": {
"workflowsTitle": "Approfondisci i flussi di lavoro.",
"upscalingTitle": "Amplia e aggiungi dettagli.",
"canvasTitle": "Modifica e perfeziona sulla tela.",
"generateTitle": "Genera immagini da prompt testuali.",
"modelGuideText": "Vuoi scoprire quali prompt funzionano meglio per ciascun modello?",
"modelGuideLink": "Consulta la nostra guida ai modelli.",
"workflows": {
"description": "I flussi di lavoro sono modelli riutilizzabili che automatizzano le attività di generazione delle immagini, consentendo di eseguire rapidamente operazioni complesse e di ottenere risultati coerenti.",
"learnMoreLink": "Scopri di più sulla creazione di flussi di lavoro",
"browseTemplates": {
"title": "Sfoglia i modelli di flusso di lavoro",
"description": "Scegli tra flussi di lavoro predefiniti per le attività comuni"
},
"createNew": {
"title": "Crea un nuovo flusso di lavoro",
"description": "Avvia un nuovo flusso di lavoro da zero"
},
"loadFromFile": {
"title": "Carica flusso di lavoro da file",
"description": "Carica un flusso di lavoro per iniziare con una configurazione esistente"
}
},
"upscaling": {
"uploadImage": {
"title": "Carica l'immagine da ampliare",
"description": "Fai clic o trascina un'immagine per ingrandirla (JPG, PNG, WebP fino a 100 MB)"
},
"replaceImage": {
"title": "Sostituisci l'immagine corrente",
"description": "Fai clic o trascina una nuova immagine per sostituire quella corrente"
},
"imageReady": {
"title": "Immagine pronta",
"description": "Premere Invoke per iniziare l'ampliamento"
},
"readyToUpscale": {
"title": "Pronto per ampliare!",
"description": "Configura le impostazioni qui sotto, quindi fai clic sul pulsante Invoke per iniziare ad ampliare l'immagine."
},
"upscaleModel": "Modello per l'ampliamento",
"model": "Modello",
"scale": "Scala",
"helpText": {
"promptAdvice": "Durante l'ampliamento, utilizza un prompt che descriva il mezzo e lo stile. Evita di descrivere dettagli specifici del contenuto dell'immagine.",
"styleAdvice": "L'ampliamento funziona meglio con lo stile generale dell'immagine."
}
}
}
},
"upscaling": {
@@ -2578,10 +2386,7 @@
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.",
"upscale": "Amplia",
"incompatibleBaseModel": "Architettura del modello principale non supportata per l'ampliamento",
"incompatibleBaseModelDesc": "L'ampliamento è supportato solo per i modelli di architettura SD1.5 e SDXL. Cambia il modello principale per abilitare l'ampliamento.",
"tileControl": "Controllo del riquadro",
"tileSize": "Dimensione del riquadro",
"tileOverlap": "Sovrapposizione riquadro"
"incompatibleBaseModelDesc": "L'ampliamento è supportato solo per i modelli di architettura SD1.5 e SDXL. Cambia il modello principale per abilitare l'ampliamento."
},
"upsell": {
"inviteTeammates": "Invita collaboratori",
@@ -2631,8 +2436,7 @@
"positivePromptColumn": "'prompt' o 'positive_prompt'",
"noTemplates": "Nessun modello",
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
"promptTemplateCleared": "Modello di prompt cancellato",
"togglePromptPreviews": "Attiva/disattiva le anteprime dei prompt"
"promptTemplateCleared": "Modello di prompt cancellato"
},
"newUserExperience": {
"gettingStartedSeries": "Desideri maggiori informazioni? Consulta la nostra <LinkComponent>Getting Started Series</LinkComponent> per suggerimenti su come sfruttare appieno il potenziale di Invoke Studio.",
@@ -2648,10 +2452,8 @@
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"items": [
"Nuova impostazione per inviare tutte le generazioni della Tela direttamente alla Galleria.",
"Nuove funzionalità Inverti maschera (Maiusc+V) e Adatta il Riquadro di delimitazione alla maschera (Maiusc+B).",
"Supporto esteso per miniature e configurazioni dei modelli.",
"Vari altri aggiornamenti e correzioni per la qualità della vita"
"Inpainting: livelli di rumore per maschera e limiti di denoise.",
"Canvas: proporzioni più intelligenti per SDXL e scorrimento e zoom migliorati."
]
},
"system": {
@@ -2683,18 +2485,64 @@
"supportVideos": {
"gettingStarted": "Iniziare",
"supportVideos": "Video di supporto",
"watch": "Guarda",
"studioSessionsDesc": "Unisciti al nostro <DiscordLink /> per partecipare alle sessioni live e porre domande. Le sessioni vengono caricate nella playlist la settimana successiva.",
"videos": {
"gettingStarted": {
"title": "Introduzione a Invoke",
"description": "Serie video completa che copre tutto ciò che devi sapere per iniziare a usare Invoke, dalla creazione della tua prima immagine alle tecniche avanzate."
"usingControlLayersAndReferenceGuides": {
"title": "Utilizzo di livelli di controllo e guide di riferimento",
"description": "Scopri come guidare la creazione delle tue immagini con livelli di controllo e immagini di riferimento."
},
"studioSessions": {
"title": "Sessioni in studio",
"description": "Sessioni approfondite che esplorano le funzionalità avanzate di Invoke, i flussi di lavoro creativi e le discussioni della community."
"creatingYourFirstImage": {
"description": "Introduzione alla creazione di un'immagine da zero utilizzando gli strumenti di Invoke.",
"title": "Creazione della tua prima immagine"
},
"understandingImageToImageAndDenoising": {
"description": "Panoramica delle trasformazioni immagine-a-immagine e della riduzione del rumore in Invoke.",
"title": "Comprendere immagine-a-immagine e riduzione del rumore"
},
"howDoIDoImageToImageTransformation": {
"description": "Tutorial su come eseguire trasformazioni da immagine a immagine in Invoke.",
"title": "Come si esegue la trasformazione da immagine-a-immagine?"
},
"howDoIUseInpaintMasks": {
"title": "Come si usano le maschere Inpaint?",
"description": "Come applicare maschere inpaint per la correzione e la variazione delle immagini."
},
"howDoIOutpaint": {
"description": "Guida all'outpainting oltre i confini dell'immagine originale.",
"title": "Come posso eseguire l'outpainting?"
},
"exploringAIModelsAndConceptAdapters": {
"description": "Approfondisci i modelli di intelligenza artificiale e scopri come utilizzare gli adattatori concettuali per il controllo creativo.",
"title": "Esplorazione dei modelli di IA e degli adattatori concettuali"
},
"upscaling": {
"title": "Ampliamento",
"description": "Come ampliare le immagini con gli strumenti di Invoke per migliorarne la risoluzione."
},
"creatingAndComposingOnInvokesControlCanvas": {
"description": "Impara a comporre immagini utilizzando la tela di controllo di Invoke.",
"title": "Creare e comporre sulla tela di controllo di Invoke"
},
"howDoIGenerateAndSaveToTheGallery": {
"description": "Passaggi per generare e salvare le immagini nella galleria.",
"title": "Come posso generare e salvare nella Galleria?"
},
"howDoIEditOnTheCanvas": {
"title": "Come posso apportare modifiche sulla tela?",
"description": "Guida alla modifica delle immagini direttamente sulla tela."
},
"howDoIUseControlNetsAndControlLayers": {
"title": "Come posso utilizzare le Reti di Controllo e i Livelli di Controllo?",
"description": "Impara ad applicare livelli di controllo e reti di controllo alle tue immagini."
},
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
"title": "Come si utilizzano gli adattatori IP globali e le immagini di riferimento?",
"description": "Introduzione all'aggiunta di immagini di riferimento e adattatori IP globali."
}
}
},
"controlCanvas": "Tela di Controllo",
"watch": "Guarda",
"studioSessionsDesc1": "Dai un'occhiata a <StudioSessionsPlaylistLink /> per approfondimenti su Invoke.",
"studioSessionsDesc2": "Unisciti al nostro <DiscordLink /> per partecipare alle sessioni live e fare domande. Le sessioni vengono caricate sulla playlist la settimana successiva."
},
"modelCache": {
"clear": "Cancella la cache del modello",

View File

@@ -141,7 +141,7 @@
"loading": "ロード中",
"currentlyInUse": "この画像は現在下記の機能を使用しています:",
"drop": "ドロップ",
"dropOrUpload": "ドロップまたはアップロード",
"dropOrUpload": "$t(gallery.drop) またはアップロード",
"deleteImage_other": "画像 {{count}} 枚を削除",
"deleteImagePermanent": "削除された画像は復元できません。",
"download": "ダウンロード",
@@ -193,8 +193,7 @@
"images": "画像",
"assetsTab": "プロジェクトで使用するためにアップロードされたファイル。",
"imagesTab": "Invoke内で作成および保存された画像。",
"assets": "アセット",
"useForPromptGeneration": "プロンプト生成に使用する"
"assets": "アセット"
},
"hotkeys": {
"searchHotkeys": "ホットキーを検索",
@@ -364,16 +363,6 @@
"selectRectTool": {
"title": "矩形ツール",
"desc": "矩形ツールを選択します。"
},
"settings": {
"behavior": "行動",
"display": "ディスプレイ",
"grid": "グリッド",
"debug": "デバッグ"
},
"toggleNonRasterLayers": {
"title": "非ラスターレイヤーの切り替え",
"desc": "ラスター以外のレイヤー カテゴリ (コントロール レイヤー、インペイント マスク、地域ガイダンス) を表示または非表示にします。"
}
},
"workflows": {
@@ -641,7 +630,7 @@
"restoreDefaultSettings": "クリックするとモデルのデフォルト設定が使用されます.",
"hfTokenSaved": "ハギングフェイストークンを保存しました",
"imageEncoderModelId": "画像エンコーダーモデルID",
"includesNModels": "{{n}}個のモデルとこれらの依存関係を含みます",
"includesNModels": "{{n}}個のモデルとこれらの依存関係を含みます",
"learnMoreAboutSupportedModels": "私たちのサポートしているモデルについて更に学ぶ",
"modelImageUpdateFailed": "モデル画像アップデート失敗",
"scanFolder": "スキャンフォルダ",
@@ -665,30 +654,7 @@
"manageModels": "モデル管理",
"hfTokenReset": "ハギングフェイストークンリセット",
"relatedModels": "関連のあるモデル",
"showOnlyRelatedModels": "関連している",
"installedModelsCount": "{{total}} モデルのうち {{installed}} 個がインストールされています。",
"allNModelsInstalled": "{{count}} 個のモデルがすべてインストールされています",
"nToInstall": "{{count}}個をインストールする",
"nAlreadyInstalled": "{{count}} 個すでにインストールされています",
"bundleAlreadyInstalled": "バンドルがすでにインストールされています",
"bundleAlreadyInstalledDesc": "{{bundleName}} バンドル内のすべてのモデルはすでにインストールされています。",
"launchpadTab": "ランチパッド",
"launchpad": {
"welcome": "モデルマネジメントへようこそ",
"description": "Invoke プラットフォームのほとんどの機能を利用するには、モデルのインストールが必要です。手動インストールオプションから選択するか、厳選されたスターターモデルをご覧ください。",
"manualInstall": "マニュアルインストール",
"urlDescription": "URLまたはローカルファイルパスからモデルをインストールします。特定のモデルを追加したい場合に最適です。",
"huggingFaceDescription": "HuggingFace リポジトリからモデルを直接参照してインストールします。",
"scanFolderDescription": "ローカルフォルダをスキャンしてモデルを自動的に検出し、インストールします。",
"recommendedModels": "推奨モデル",
"exploreStarter": "または、利用可能なすべてのスターターモデルを参照してください",
"quickStart": "クイックスタートバンドル",
"bundleDescription": "各バンドルには各モデルファミリーの必須モデルと、開始するための厳選されたベースモデルが含まれています。",
"browseAll": "または、利用可能なすべてのモデルを参照してください。",
"stableDiffusion15": "Stable Diffusion1.5",
"sdxl": "SDXL",
"fluxDev": "FLUX.1 dev"
}
"showOnlyRelatedModels": "関連している"
},
"parameters": {
"images": "画像",
@@ -754,10 +720,7 @@
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bboxの高さは{{height}}です",
"noFLUXVAEModelSelected": "FLUX生成にVAEモデルが選択されていません",
"noT5EncoderModelSelected": "FLUX生成にT5エンコーダモデルが選択されていません",
"modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アカウント設定にアクセスしてアップグレードしてください。",
"fluxKontextMultipleReferenceImages": "Flux Kontext では一度に 1 つの参照画像しか使用できません",
"promptExpansionPending": "プロンプト拡張が進行中",
"promptExpansionResultPending": "プロンプト拡張結果を受け入れるか破棄してください"
"modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アカウント設定にアクセスしてアップグレードしてください。"
},
"aspect": "縦横比",
"lockAspectRatio": "縦横比を固定",
@@ -912,26 +875,7 @@
"imageNotLoadedDesc": "画像を見つけられません",
"parameterNotSetDesc": "{{parameter}}を呼び出せません",
"chatGPT4oIncompatibleGenerationMode": "ChatGPT 4oは,テキストから画像への生成と画像から画像への生成のみをサポートしています.インペインティングおよび,アウトペインティングタスクには他のモデルを使用してください.",
"imagenIncompatibleGenerationMode": "Google {{model}} はテキストから画像への変換のみをサポートしています. 画像から画像への変換, インペインティング,アウトペインティングのタスクには他のモデルを使用してください.",
"noRasterLayers": "ラスターレイヤーが見つかりません",
"noRasterLayersDesc": "PSDにエクスポートするには、少なくとも1つのラスターレイヤーを作成します",
"noActiveRasterLayers": "アクティブなラスターレイヤーがありません",
"noActiveRasterLayersDesc": "PSD にエクスポートするには、少なくとも 1 つのラスター レイヤーを有効にします",
"noVisibleRasterLayers": "表示されるラスター レイヤーがありません",
"noVisibleRasterLayersDesc": "PSD にエクスポートするには、少なくとも 1 つのラスター レイヤーを有効にします",
"invalidCanvasDimensions": "キャンバスのサイズが無効です",
"canvasTooLarge": "キャンバスが大きすぎます",
"canvasTooLargeDesc": "キャンバスのサイズがPSDエクスポートの最大許容サイズを超えています。キャンバス全体の幅と高さを小さくしてから、もう一度お試しください。",
"failedToProcessLayers": "レイヤーの処理に失敗しました",
"psdExportSuccess": "PSDエクスポート完了",
"psdExportSuccessDesc": "{{count}} 個のレイヤーを PSD ファイルに正常にエクスポートしました",
"problemExportingPSD": "PSD のエクスポート中に問題が発生しました",
"canvasManagerNotAvailable": "キャンバスマネージャーは利用できません",
"noValidLayerAdapters": "有効なレイヤーアダプタが見つかりません",
"fluxKontextIncompatibleGenerationMode": "Flux Kontext はテキストから画像への変換のみをサポートしています。画像から画像への変換、インペインティング、アウトペインティングのタスクには他のモデルを使用してください。",
"promptGenerationStarted": "プロンプト生成が開始されました",
"uploadAndPromptGenerationFailed": "画像のアップロードとプロンプトの生成に失敗しました",
"promptExpansionFailed": "プロンプト拡張に失敗しました"
"imagenIncompatibleGenerationMode": "Google {{model}} はテキストから画像への変換のみをサポートしています. 画像から画像への変換, インペインティング,アウトペインティングのタスクには他のモデルを使用してください."
},
"accessibility": {
"invokeProgressBar": "進捗バー",
@@ -1070,8 +1014,7 @@
"lora": "LoRA",
"defaultVAE": "デフォルトVAE",
"noLoRAsInstalled": "インストールされているLoRAはありません",
"noRefinerModelsInstalled": "インストールされているSDXLリファイナーモデルはありません",
"noCompatibleLoRAs": "互換性のあるLoRAはありません"
"noRefinerModelsInstalled": "インストールされているSDXLリファイナーモデルはありません"
},
"nodes": {
"addNode": "ノードを追加",
@@ -1765,16 +1708,7 @@
"prompt": {
"addPromptTrigger": "プロンプトトリガーを追加",
"compatibleEmbeddings": "互換性のある埋め込み",
"noMatchingTriggers": "一致するトリガーがありません",
"generateFromImage": "画像からプロンプトを生成する",
"expandCurrentPrompt": "現在のプロンプトを展開",
"uploadImageForPromptGeneration": "プロンプト生成用の画像をアップロードする",
"expandingPrompt": "プロンプトを展開しています...",
"resultTitle": "プロンプト拡張完了",
"resultSubtitle": "拡張プロンプトの処理方法を選択します:",
"replace": "交換する",
"insert": "挿入する",
"discard": "破棄する"
"noMatchingTriggers": "一致するトリガーがありません"
},
"ui": {
"tabs": {
@@ -1782,61 +1716,7 @@
"canvas": "キャンバス",
"workflows": "ワークフロー",
"models": "モデル",
"gallery": "ギャラリー",
"generation": "生成",
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"upscaling": "アップスケーリング",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
},
"launchpad": {
"upscaling": {
"model": "モデル",
"scale": "スケール",
"helpText": {
"promptAdvice": "アップスケールする際は、媒体とスタイルを説明するプロンプトを使用してください。画像内の具体的なコンテンツの詳細を説明することは避けてください。",
"styleAdvice": "アップスケーリングは、画像の全体的なスタイルに最適です。"
},
"uploadImage": {
"title": "アップスケール用の画像をアップロードする",
"description": "アップスケールするには、画像をクリックまたはドラッグしますJPG、PNG、WebP、最大100MB"
},
"replaceImage": {
"title": "現在の画像を置き換える",
"description": "新しい画像をクリックまたはドラッグして、現在の画像を置き換えます"
},
"imageReady": {
"title": "画像準備完了",
"description": "アップスケールを開始するにはInvokeを押してください"
},
"readyToUpscale": {
"title": "アップスケールの準備ができました!",
"description": "以下の設定を構成し、「Invoke」ボタンをクリックして画像のアップスケールを開始します。"
},
"upscaleModel": "アップスケールモデル"
},
"workflowsTitle": "ワークフローを詳しく見てみましょう。",
"upscalingTitle": "アップスケールして詳細を追加します。",
"canvasTitle": "キャンバス上で編集および調整します。",
"generateTitle": "テキストプロンプトから画像を生成します。",
"modelGuideText": "各モデルに最適なプロンプトを知りたいですか?",
"modelGuideLink": "モデルガイドをご覧ください。",
"workflows": {
"description": "ワークフローは、画像生成タスクを自動化する再利用可能なテンプレートであり、複雑な操作を迅速に実行して一貫した結果を得ることができます。",
"learnMoreLink": "ワークフローの作成について詳しく見る",
"browseTemplates": {
"title": "ワークフローテンプレートを参照する",
"description": "一般的なタスク用にあらかじめ構築されたワークフローから選択する"
},
"createNew": {
"title": "新規ワークフローを作成する",
"description": "新しいワークフローをゼロから始める"
},
"loadFromFile": {
"title": "ファイルからワークフローを読み込む",
"description": "既存の設定から開始するためのワークフローをアップロードする"
}
}
"gallery": "ギャラリー"
}
},
"controlLayers": {
@@ -1852,16 +1732,7 @@
"cropCanvasToBbox": "キャンバスをバウンディングボックスでクロップ",
"newGlobalReferenceImage": "新規全域参照画像",
"newRegionalReferenceImage": "新規領域参照画像",
"canvasGroup": "キャンバス",
"saveToGalleryGroup": "ギャラリーに保存",
"saveCanvasToGallery": "キャンバスをギャラリーに保存",
"saveBboxToGallery": "Bボックスをギャラリーに保存",
"newControlLayer": "新規コントロールレイヤー",
"newRasterLayer": "新規ラスターレイヤー",
"newInpaintMask": "新規インペイントマスク",
"copyToClipboard": "クリップボードにコピー",
"copyCanvasToClipboard": "キャンバスをクリップボードにコピー",
"copyBboxToClipboard": "Bボックスをクリップボードにコピー"
"canvasGroup": "キャンバス"
},
"regionalGuidance": "領域ガイダンス",
"globalReferenceImage": "全域参照画像",
@@ -1872,11 +1743,7 @@
"transform": "変形",
"apply": "適用",
"cancel": "キャンセル",
"reset": "リセット",
"fitMode": "フィットモード",
"fitModeContain": "含む",
"fitModeCover": "カバー",
"fitModeFill": "満たす"
"reset": "リセット"
},
"cropLayerToBbox": "レイヤーをバウンディングボックスでクロップ",
"convertInpaintMaskTo": "$t(controlLayers.inpaintMask)を変換",
@@ -1887,8 +1754,7 @@
"rectangle": "矩形",
"move": "移動",
"eraser": "消しゴム",
"bbox": "Bbox",
"view": "ビュー"
"bbox": "Bbox"
},
"saveCanvasToGallery": "キャンバスをギャラリーに保存",
"saveBboxToGallery": "バウンディングボックスをギャラリーへ保存",
@@ -1908,386 +1774,25 @@
"removeBookmark": "ブックマークを外す",
"savedToGalleryOk": "ギャラリーに保存しました",
"controlMode": {
"prompt": "プロンプト",
"controlMode": "コントロールモード",
"balanced": "バランス(推奨)",
"control": "コントロール",
"megaControl": "メガコントロール"
"prompt": "プロンプト"
},
"prompt": "プロンプト",
"settings": {
"snapToGrid": {
"off": "オフ",
"on": "オン",
"label": "グリッドにスナップ"
},
"preserveMask": {
"label": "マスクされた領域を保持",
"alert": "マスクされた領域の保存"
},
"isolatedStagingPreview": "分離されたステージングプレビュー",
"isolatedPreview": "分離されたプレビュー",
"isolatedLayerPreview": "分離されたレイヤーのプレビュー",
"isolatedLayerPreviewDesc": "フィルタリングや変換などの操作を実行するときに、このレイヤーのみを表示するかどうか。",
"invertBrushSizeScrollDirection": "ブラシサイズのスクロール反転",
"pressureSensitivity": "圧力感度"
"on": "オン"
}
},
"filter": {
"filter": "フィルター",
"spandrel_filter": {
"model": "モデル",
"label": "img2imgモデル",
"description": "選択したレイヤーでimg2imgモデルを実行します。",
"autoScale": "オートスケール",
"autoScaleDesc": "選択したモデルは、目標スケールに達するまで実行されます。",
"scale": "ターゲットスケール"
"model": "モデル"
},
"apply": "適用",
"reset": "リセット",
"cancel": "キャンセル",
"filters": "フィルター",
"filterType": "フィルタータイプ",
"autoProcess": "オートプロセス",
"process": "プロセス",
"advanced": "アドバンスド",
"processingLayerWith": "{{type}} フィルターを使用した処理レイヤー。",
"forMoreControl": "さらに細かく制御するには、以下の「詳細設定」をクリックしてください。",
"canny_edge_detection": {
"label": "キャニーエッジ検出",
"description": "Canny エッジ検出アルゴリズムを使用して、選択したレイヤーからエッジ マップを生成します。",
"low_threshold": "低閾値",
"high_threshold": "高閾値"
},
"color_map": {
"label": "カラーマップ",
"description": "選択したレイヤーからカラーマップを作成します。",
"tile_size": "タイルサイズ"
},
"content_shuffle": {
"label": "コンテンツシャッフル",
"description": "選択したレイヤーのコンテンツを、「液化」効果と同様にシャッフルします。",
"scale_factor": "スケール係数"
},
"depth_anything_depth_estimation": {
"label": "デプスエニシング",
"description": "デプスエニシングモデルを使用して、選択したレイヤーから深度マップを生成します。",
"model_size": "モデルサイズ",
"model_size_small": "スモール",
"model_size_small_v2": "スモールv2",
"model_size_base": "ベース",
"model_size_large": "ラージ"
},
"dw_openpose_detection": {
"label": "DW オープンポーズ検出",
"description": "DW Openpose モデルを使用して、選択したレイヤー内の人間のポーズを検出します。",
"draw_hands": "手を描く",
"draw_face": "顔を描く",
"draw_body": "体を描く"
},
"hed_edge_detection": {
"label": "HEDエッジ検出",
"description": "HED エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。",
"scribble": "落書き"
},
"lineart_anime_edge_detection": {
"label": "線画アニメのエッジ検出",
"description": "線画アニメエッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。"
},
"lineart_edge_detection": {
"label": "線画エッジ検出",
"description": "線画エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。",
"coarse": "粗い"
},
"mediapipe_face_detection": {
"label": "メディアパイプ顔検出",
"description": "メディアパイプ顔検出モデルを使用して、選択したレイヤー内の顔を検出します。",
"max_faces": "マックスフェイス",
"min_confidence": "最小信頼度"
},
"mlsd_detection": {
"label": "線分検出",
"description": "MLSD 線分検出モデルを使用して、選択したレイヤーから線分マップを生成します。",
"score_threshold": "スコア閾値",
"distance_threshold": "距離閾値"
},
"normal_map": {
"label": "ノーマルマップ",
"description": "選択したレイヤーからノーマルマップを生成します。"
},
"pidi_edge_detection": {
"label": "PiDiNetエッジ検出",
"description": "PiDiNet エッジ検出モデルを使用して、選択したレイヤーからエッジ マップを生成します。",
"scribble": "落書き",
"quantize_edges": "エッジを量子化する"
},
"img_blur": {
"label": "画像をぼかす",
"description": "選択したレイヤーをぼかします。",
"blur_type": "ぼかしの種類",
"blur_radius": "半径",
"gaussian_type": "ガウス分布",
"box_type": "ボックス"
},
"img_noise": {
"label": "ノイズ画像",
"description": "選択したレイヤーにノイズを追加します。",
"noise_type": "ノイズの種類",
"noise_amount": "総計",
"gaussian_type": "ガウス分布",
"salt_and_pepper_type": "塩コショウ",
"noise_color": "カラーノイズ",
"size": "ノイズサイズ"
},
"adjust_image": {
"label": "画像を調整する",
"description": "画像の選択したチャンネルを調整します。",
"channel": "チャンネル",
"value_setting": "バリュー",
"scale_values": "スケールバリュー",
"red": "赤RGBA",
"green": "緑RGBA",
"blue": "青RGBA",
"alpha": "アルファRGBA",
"cyan": "シアンCMYK",
"magenta": "マゼンタCMYK",
"yellow": "黄色CMYK",
"black": "黒CMYK",
"hue": "色相HSV",
"saturation": "彩度HSV",
"value": "値HSV",
"luminosity": "明度LAB",
"a": "Aラボ",
"b": "Bラボ",
"y": "YYCbCr",
"cb": "CbYCbCr",
"cr": "CrYCbCr"
}
"cancel": "キャンセル"
},
"weight": "重み",
"bookmark": "クイックスイッチのブックマーク",
"exportCanvasToPSD": "キャンバスをPSDにエクスポート",
"savedToGalleryError": "ギャラリーへの保存中にエラーが発生しました",
"regionCopiedToClipboard": "{{region}} をクリップボードにコピーしました",
"copyRegionError": "{{region}} のコピー中にエラーが発生しました",
"newGlobalReferenceImageOk": "作成されたグローバル参照画像",
"newGlobalReferenceImageError": "グローバル参照イメージの作成中に問題が発生しました",
"newRegionalReferenceImageOk": "地域参照画像の作成",
"newRegionalReferenceImageError": "地域参照画像の作成中に問題が発生しました",
"newControlLayerOk": "制御レイヤーの作成",
"newControlLayerError": "制御層の作成中に問題が発生しました",
"newRasterLayerOk": "ラスターレイヤーを作成しました",
"newRasterLayerError": "ラスターレイヤーの作成中に問題が発生しました",
"pullBboxIntoLayerOk": "Bbox をレイヤーにプル",
"pullBboxIntoLayerError": "BBox をレイヤーにプルする際に問題が発生しました",
"pullBboxIntoReferenceImageOk": "Bbox が ReferenceImage にプルされました",
"pullBboxIntoReferenceImageError": "BBox を ReferenceImage にプルする際に問題が発生しました",
"regionIsEmpty": "選択した領域は空です",
"mergeVisible": "マージを可視化",
"mergeVisibleOk": "マージされたレイヤー",
"mergeVisibleError": "レイヤーの結合エラー",
"mergingLayers": "レイヤーのマージ",
"clearHistory": "履歴をクリア",
"bboxOverlay": "Bboxオーバーレイを表示",
"ruleOfThirds": "三分割法を表示",
"newSession": "新しいセッション",
"clearCaches": "キャッシュをクリア",
"recalculateRects": "長方形を再計算する",
"clipToBbox": "ストロークをBboxにクリップ",
"outputOnlyMaskedRegions": "生成された領域のみを出力する",
"width": "幅",
"autoNegative": "オートネガティブ",
"enableAutoNegative": "オートネガティブを有効にする",
"disableAutoNegative": "オートネガティブを無効にする",
"deletePrompt": "プロンプトを削除",
"deleteReferenceImage": "参照画像を削除",
"showHUD": "HUDを表示",
"maskFill": "マスク塗りつぶし",
"addPositivePrompt": "$t(controlLayers.prompt) を追加します",
"addNegativePrompt": "$t(controlLayers.negativePrompt)を追加します",
"addReferenceImage": "$t(controlLayers.referenceImage)を追加します",
"addImageNoise": "$t(controlLayers.imageNoise)を追加します",
"addRasterLayer": "$t(controlLayers.rasterLayer)を追加します",
"addControlLayer": "$t(controlLayers.controlLayer)を追加します",
"addInpaintMask": "$t(controlLayers.inpaintMask)を追加します",
"addRegionalGuidance": "$t(controlLayers.regionalGuidance)を追加します",
"addGlobalReferenceImage": "$t(controlLayers.globalReferenceImage)を追加します",
"addDenoiseLimit": "$t(controlLayers.denoiseLimit)を追加します",
"controlLayer": "コントロールレイヤー",
"inpaintMask": "インペイントマスク",
"referenceImageRegional": "参考画像(地域別)",
"referenceImageGlobal": "参考画像(グローバル)",
"asRasterLayer": "$t(controlLayers.rasterLayer) として",
"asRasterLayerResize": "$t(controlLayers.rasterLayer) として (リサイズ)",
"asControlLayer": "$t(controlLayers.controlLayer) として",
"asControlLayerResize": "$t(controlLayers.controlLayer) として (リサイズ)",
"referenceImage": "参照画像",
"sendingToCanvas": "キャンバスに生成をのせる",
"sendingToGallery": "生成をギャラリーに送る",
"sendToGallery": "ギャラリーに送る",
"sendToGalleryDesc": "Invokeを押すとユニークな画像が生成され、ギャラリーに保存されます。",
"sendToCanvas": "キャンバスに送る",
"newLayerFromImage": "画像から新規レイヤー",
"newCanvasFromImage": "画像から新規キャンバス",
"newImg2ImgCanvasFromImage": "画像からの新規 Img2Img",
"copyToClipboard": "クリップボードにコピー",
"sendToCanvasDesc": "Invokeを押すと、進行中の作品がキャンバス上にステージされます。",
"viewProgressInViewer": "<Btn>画像ビューア</Btn>で進行状況と出力を表示します。",
"viewProgressOnCanvas": "<Btn>キャンバス</Btn> で進行状況とステージ出力を表示します。",
"rasterLayer_withCount_other": "ラスターレイヤー",
"controlLayer_withCount_other": "コントロールレイヤー",
"regionalGuidance_withCount_hidden": "地域ガイダンス({{count}} 件非表示)",
"controlLayers_withCount_hidden": "コントロールレイヤー({{count}} 個非表示)",
"rasterLayers_withCount_hidden": "ラスター レイヤー ({{count}} 個非表示)",
"globalReferenceImages_withCount_hidden": "グローバル参照画像({{count}} 枚非表示)",
"regionalGuidance_withCount_visible": "地域ガイダンス ({{count}})",
"controlLayers_withCount_visible": "コントロールレイヤー ({{count}})",
"rasterLayers_withCount_visible": "ラスターレイヤー({{count}}",
"globalReferenceImages_withCount_visible": "グローバル参照画像 ({{count}})",
"layer_other": "レイヤー",
"layer_withCount_other": "レイヤー ({{count}})",
"convertRasterLayerTo": "$t(controlLayers.rasterLayer) を変換する",
"convertControlLayerTo": "$t(controlLayers.controlLayer) を変換する",
"convertRegionalGuidanceTo": "$t(controlLayers.regionalGuidance) を変換する",
"copyRasterLayerTo": "$t(controlLayers.rasterLayer)をコピーする",
"copyControlLayerTo": "$t(controlLayers.controlLayer) をコピーする",
"copyRegionalGuidanceTo": "$t(controlLayers.regionalGuidance)をコピーする",
"newRasterLayer": "新しい $t(controlLayers.rasterLayer)",
"newControlLayer": "新しい $t(controlLayers.controlLayer)",
"newInpaintMask": "新しい $t(controlLayers.inpaintMask)",
"newRegionalGuidance": "新しい $t(controlLayers.regionalGuidance)",
"pasteTo": "貼り付け先",
"pasteToAssets": "アセット",
"pasteToAssetsDesc": "アセットに貼り付け",
"pasteToBbox": "Bボックス",
"pasteToBboxDesc": "新しいレイヤーBbox内",
"pasteToCanvas": "キャンバス",
"pasteToCanvasDesc": "新しいレイヤー(キャンバス内)",
"pastedTo": "{{destination}} に貼り付けました",
"transparency": "透明性",
"enableTransparencyEffect": "透明効果を有効にする",
"disableTransparencyEffect": "透明効果を無効にする",
"hidingType": "{{type}} を非表示",
"showingType": "{{type}}を表示",
"showNonRasterLayers": "非ラスターレイヤーを表示 (Shift+H)",
"hideNonRasterLayers": "非ラスターレイヤーを非表示にする (Shift+H)",
"dynamicGrid": "ダイナミックグリッド",
"logDebugInfo": "デバッグ情報をログに記録する",
"locked": "ロックされています",
"unlocked": "ロック解除",
"deleteSelected": "選択項目を削除",
"stagingOnCanvas": "ステージング画像",
"replaceLayer": "レイヤーの置き換え",
"pullBboxIntoLayer": "Bboxをレイヤーに引き込む",
"pullBboxIntoReferenceImage": "Bboxを参照画像に取り込む",
"showProgressOnCanvas": "キャンバスに進捗状況を表示",
"useImage": "画像を使う",
"negativePrompt": "ネガティブプロンプト",
"beginEndStepPercentShort": "開始/終了 %",
"newGallerySession": "新しいギャラリーセッション",
"newGallerySessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成した画像はギャラリーに送信されます。",
"newCanvasSession": "新規キャンバスセッション",
"newCanvasSessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成はキャンバス上でステージングされます。",
"resetCanvasLayers": "キャンバスレイヤーをリセット",
"resetGenerationSettings": "生成設定をリセット",
"replaceCurrent": "現在のものを置き換える",
"controlLayerEmptyState": "<UploadButton>画像をアップロード</UploadButton>、<GalleryButton>ギャラリー</GalleryButton>からこのレイヤーに画像をドラッグ、<PullBboxButton>境界ボックスをこのレイヤーにプル</PullBboxButton>、またはキャンバスに描画して開始します。",
"referenceImageEmptyStateWithCanvasOptions": "開始するには、<UploadButton>画像をアップロード</UploadButton>するか、<GalleryButton>ギャラリー</GalleryButton>からこの参照画像に画像をドラッグするか、<PullBboxButton>境界ボックスをこの参照画像にプル</PullBboxButton>します。",
"referenceImageEmptyState": "開始するには、<UploadButton>画像をアップロード</UploadButton>するか、<GalleryButton>ギャラリー</GalleryButton>からこの参照画像に画像をドラッグします。",
"uploadOrDragAnImage": "ギャラリーから画像をドラッグするか、<UploadButton>画像をアップロード</UploadButton>します。",
"imageNoise": "画像ノイズ",
"denoiseLimit": "ノイズ除去制限",
"warnings": {
"problemsFound": "問題が見つかりました",
"unsupportedModel": "選択したベースモデルではレイヤーがサポートされていません",
"controlAdapterNoModelSelected": "制御レイヤーモデルが選択されていません",
"controlAdapterIncompatibleBaseModel": "互換性のない制御レイヤーベースモデル",
"controlAdapterNoControl": "コントロールが選択/描画されていません",
"ipAdapterNoModelSelected": "参照画像モデルが選択されていません",
"ipAdapterIncompatibleBaseModel": "互換性のない参照画像ベースモデル",
"ipAdapterNoImageSelected": "参照画像が選択されていません",
"rgNoPromptsOrIPAdapters": "テキストプロンプトや参照画像はありません",
"rgNegativePromptNotSupported": "選択されたベースモデルでは否定プロンプトはサポートされていません",
"rgReferenceImagesNotSupported": "選択されたベースモデルでは地域の参照画像はサポートされていません",
"rgAutoNegativeNotSupported": "選択したベースモデルでは自動否定はサポートされていません",
"rgNoRegion": "領域が描画されていません",
"fluxFillIncompatibleWithControlLoRA": "コントロールLoRAはFLUX Fillと互換性がありません"
},
"errors": {
"unableToFindImage": "画像が見つかりません",
"unableToLoadImage": "画像を読み込めません"
},
"ipAdapterMethod": {
"ipAdapterMethod": "モード",
"full": "スタイルと構成",
"fullDesc": "視覚スタイル (色、テクスチャ) と構成 (レイアウト、構造) を適用します。",
"style": "スタイル(シンプル)",
"styleDesc": "レイアウトを考慮せずに視覚スタイル(色、テクスチャ)を適用します。以前は「スタイルのみ」と呼ばれていました。",
"composition": "構成のみ",
"compositionDesc": "参照スタイルを無視してレイアウトと構造を複製します。",
"styleStrong": "スタイル(ストロング)",
"styleStrongDesc": "構成への影響をわずかに抑えて、強力なビジュアル スタイルを適用します。",
"stylePrecise": "スタイル(正確)",
"stylePreciseDesc": "被写体の影響を排除し、正確な視覚スタイルを適用します。"
},
"fluxReduxImageInfluence": {
"imageInfluence": "イメージの影響力",
"lowest": "最低",
"low": "低",
"medium": "中",
"high": "高",
"highest": "最高"
},
"fill": {
"fillColor": "塗りつぶし色",
"fillStyle": "塗りつぶしスタイル",
"solid": "固体",
"grid": "グリッド",
"crosshatch": "クロスハッチ",
"vertical": "垂直",
"horizontal": "水平",
"diagonal": "対角線"
},
"selectObject": {
"selectObject": "オブジェクトを選択",
"pointType": "ポイントタイプ",
"invertSelection": "選択範囲を反転",
"include": "含む",
"exclude": "除外",
"neutral": "ニュートラル",
"apply": "適用",
"reset": "リセット",
"saveAs": "名前を付けて保存",
"cancel": "キャンセル",
"process": "プロセス",
"help1": "ターゲットオブジェクトを1つ選択します。<Bold>含める</Bold>ポイントと<Bold>除外</Bold>ポイントを追加して、レイヤーのどの部分がターゲットオブジェクトの一部であるかを示します。",
"help2": "対象オブジェクト内に<Bold>含める</Bold>ポイントを1つ選択するところから始めます。ポイントを追加して選択範囲を絞り込みます。ポイントが少ないほど、通常はより良い結果が得られます。",
"help3": "選択を反転して、ターゲットオブジェクト以外のすべてを選択します。",
"clickToAdd": "レイヤーをクリックしてポイントを追加します",
"dragToMove": "ポイントをドラッグして移動します",
"clickToRemove": "ポイントをクリックして削除します"
},
"HUD": {
"bbox": "Bボックス",
"scaledBbox": "スケールされたBボックス",
"entityStatus": {
"isFiltering": "{{title}} はフィルタリング中です",
"isTransforming": "{{title}}は変化しています",
"isLocked": "{{title}}はロックされています",
"isHidden": "{{title}}は非表示になっています",
"isDisabled": "{{title}}は無効です",
"isEmpty": "{{title}} は空です"
}
},
"stagingArea": {
"accept": "受け入れる",
"discardAll": "すべて破棄",
"discard": "破棄する",
"previous": "前へ",
"next": "次へ",
"saveToGallery": "ギャラリーに保存",
"showResultsOn": "結果を表示",
"showResultsOff": "結果を隠す"
}
"weight": "重み"
},
"stylePresets": {
"clearTemplateSelection": "選択したテンプレートをクリア",
@@ -2305,56 +1810,13 @@
"nameColumn": "'name'",
"type": "タイプ",
"private": "プライベート",
"name": "名称",
"active": "アクティブ",
"copyTemplate": "テンプレートをコピー",
"deleteImage": "画像を削除",
"deleteTemplate": "テンプレートを削除",
"deleteTemplate2": "このテンプレートを削除してもよろしいですか? 元に戻すことはできません。",
"exportPromptTemplates": "プロンプトテンプレートをエクスポートするCSV",
"editTemplate": "テンプレートを編集",
"exportDownloaded": "エクスポートをダウンロードしました",
"exportFailed": "生成とCSVのダウンロードができません",
"importTemplates": "プロンプトテンプレートのインポートCSV/JSON",
"acceptedColumnsKeys": "受け入れられる列/キー:",
"positivePromptColumn": "'プロンプト'または'ポジティブプロンプト'",
"insertPlaceholder": "プレースホルダーを挿入",
"negativePrompt": "ネガティブプロンプト",
"noTemplates": "テンプレートがありません",
"noMatchingTemplates": "マッチするテンプレートがありません",
"promptTemplatesDesc1": "プロンプトテンプレートは、プロンプトボックスに書き込むプロンプトにテキストを追加します。",
"promptTemplatesDesc2": "テンプレート内でプロンプトを含める場所を指定するには <Pre>{{placeholder}}</Pre> のプレースホルダーの文字列を使用します。",
"promptTemplatesDesc3": "プレースホルダーを省略すると、テンプレートはプロンプトの末尾に追加されます。",
"positivePrompt": "ポジティブプロンプト",
"shared": "共有",
"sharedTemplates": "テンプレートを共有",
"templateDeleted": "プロンプトテンプレートを削除しました",
"unableToDeleteTemplate": "プロンプトテンプレートを削除できません",
"updatePromptTemplate": "プロンプトテンプレートをアップデート",
"useForTemplate": "プロンプトテンプレートに使用する",
"viewList": "テンプレートリストを表示",
"viewModeTooltip": "現在選択されているテンプレートでは、プロンプトはこのようになります。プロンプトを編集するには、テキストボックス内の任意の場所をクリックしてください。",
"togglePromptPreviews": "プロンプトプレビューを切り替える"
"name": "名称"
},
"upscaling": {
"upscaleModel": "アップスケールモデル",
"postProcessingModel": "ポストプロセスモデル",
"upscale": "アップスケール",
"scale": "スケール",
"creativity": "創造性",
"exceedsMaxSize": "アップスケール設定が最大サイズ制限を超えています",
"exceedsMaxSizeDetails": "アップスケールの上限は{{max Upscale Dimension}} x {{max Upscale Dimension}}ピクセルです。画像を小さくするか、スケールの選択範囲を小さくしてください。",
"structure": "構造",
"postProcessingMissingModelWarning": "後処理 (img2img) モデルをインストールするには、<LinkComponent>モデル マネージャー</LinkComponent> にアクセスしてください。",
"missingModelsWarning": "必要なモデルをインストールするには、<LinkComponent>モデル マネージャー</LinkComponent> にアクセスしてください。",
"mainModelDesc": "メインモデルSD1.5またはSDXLアーキテクチャ",
"tileControlNetModelDesc": "選択したメインモデルアーキテクチャのタイルコントロールネットモデル",
"upscaleModelDesc": "アップスケールimg2imgモデル",
"missingUpscaleInitialImage": "アップスケール用の初期画像がありません",
"missingUpscaleModel": "アップスケールモデルがありません",
"missingTileControlNetModel": "有効なタイル コントロールネットモデルがインストールされていません",
"incompatibleBaseModel": "アップスケーリングにサポートされていないメインモデルアーキテクチャです",
"incompatibleBaseModelDesc": "アップスケーリングはSD1.5およびSDXLアーキテクチャモデルでのみサポートされています。アップスケーリングを有効にするには、メインモデルを変更してください。"
"scale": "スケール"
},
"sdxl": {
"denoisingStrength": "ノイズ除去強度",
@@ -2429,34 +1891,7 @@
"minimum": "最小",
"publish": "公開",
"unpublish": "非公開",
"publishedWorkflowInputs": "インプット",
"workflowLocked": "ワークフローがロックされました",
"workflowLockedPublished": "公開済みのワークフローは編集用にロックされています。\nワークフローを非公開にして編集したり、コピーを作成したりできます。",
"workflowLockedDuringPublishing": "公開の構成中にワークフローがロックされます。",
"selectOutputNode": "出力ノードを選択",
"changeOutputNode": "出力ノードの変更",
"unpublishableInputs": "これらの公開できない入力は省略されます",
"noPublishableInputs": "公開可能な入力はありません",
"noOutputNodeSelected": "出力ノードが選択されていません",
"cannotPublish": "ワークフローを公開できません",
"publishWarnings": "警告",
"errorWorkflowHasUnsavedChanges": "ワークフローに保存されていない変更があります",
"errorWorkflowHasUnpublishableNodes": "ワークフローにはバッチ、ジェネレータ、またはメタデータ抽出ノードがあります",
"errorWorkflowHasInvalidGraph": "ワークフロー グラフが無効です (詳細については [呼び出し] ボタンにマウスを移動してください)",
"errorWorkflowHasNoOutputNode": "出力ノードが選択されていません",
"warningWorkflowHasNoPublishableInputFields": "公開可能な入力フィールドが選択されていません - 公開されたワークフローはデフォルト値のみで実行されます",
"warningWorkflowHasUnpublishableInputFields": "ワークフローには公開できない入力がいくつかあります。これらは公開されたワークフローから省略されます",
"publishFailed": "公開失敗",
"publishFailedDesc": "ワークフローの公開中に問題が発生しました。もう一度お試しください。",
"publishSuccess": "ワークフローを公開しています",
"publishSuccessDesc": "<LinkComponent>プロジェクト ダッシュボード</LinkComponent> をチェックして進捗状況を確認してください。",
"publishInProgress": "公開中",
"publishedWorkflowIsLocked": "公開されたワークフローはロックされています",
"publishingValidationRun": "公開検証実行",
"publishingValidationRunInProgress": "公開検証の実行が進行中です。",
"publishedWorkflowsLocked": "公開済みのワークフローはロックされており、編集または実行できません。このワークフローを編集または実行するには、ワークフローを非公開にするか、コピーを保存してください。",
"selectingOutputNode": "出力ノードの選択",
"selectingOutputNodeDesc": "ノードをクリックして、ワークフローの出力ノードとして選択します。"
"publishedWorkflowInputs": "インプット"
},
"chooseWorkflowFromLibrary": "ライブラリからワークフローを選択",
"unnamedWorkflow": "名前のないワークフロー",
@@ -2519,23 +1954,15 @@
"models": "モデル",
"canvas": "キャンバス",
"metadata": "メタデータ",
"queue": "キュー",
"logNamespaces": "ログのネームスペース",
"dnd": "ドラッグ&ドロップ",
"config": "構成",
"generation": "生成",
"events": "イベント"
"queue": "キュー"
},
"logLevel": {
"debug": "Debug",
"info": "Info",
"error": "Error",
"fatal": "Fatal",
"warn": "Warn",
"logLevel": "ログレベル",
"trace": "追跡"
},
"enableLogging": "ログを有効にする"
"warn": "Warn"
}
},
"dynamicPrompts": {
"promptsPreview": "プロンプトプレビュー",
@@ -2551,34 +1978,5 @@
"dynamicPrompts": "ダイナミックプロンプト",
"loading": "ダイナミックプロンプトを生成...",
"maxPrompts": "最大プロンプト"
},
"upsell": {
"inviteTeammates": "チームメートを招待",
"professional": "プロフェッショナル",
"professionalUpsell": "InvokeのProfessional Editionでご利用いただけます。詳細については、こちらをクリックするか、invoke.com/pricingをご覧ください。",
"shareAccess": "共有アクセス"
},
"newUserExperience": {
"toGetStartedLocal": "始めるには、Invoke の実行に必要なモデルをダウンロードまたはインポートしてください。次に、ボックスにプロンプトを入力し、<StrongComponent>Invoke</StrongComponent> をクリックして最初の画像を生成します。プロンプトテンプレートを選択すると、結果が向上します。画像は <StrongComponent>Gallery</StrongComponent> に直接保存するか、<StrongComponent>Canvas</StrongComponent> で編集するかを選択できます。",
"toGetStarted": "開始するには、ボックスにプロンプトを入力し、<StrongComponent>Invoke</StrongComponent> をクリックして最初の画像を生成します。プロンプトテンプレートを選択すると、結果が向上します。画像は <StrongComponent>Gallery</StrongComponent> に直接保存するか、<StrongComponent>Canvas</StrongComponent> で編集するかを選択できます。",
"toGetStartedWorkflow": "開始するには、左側のフィールドに入力し、<StrongComponent>Invoke</StrongComponent> をクリックして画像を生成します。他のワークフローも試してみたい場合は、ワークフロータイトルの横にある<StrongComponent>フォルダアイコン</StrongComponent> をクリックすると、試せる他のテンプレートのリストが表示されます。",
"gettingStartedSeries": "さらに詳しいガイダンスが必要ですか? Invoke Studio の可能性を最大限に引き出すためのヒントについては、<LinkComponent>入門シリーズ</LinkComponent>をご覧ください。",
"lowVRAMMode": "最高のパフォーマンスを得るには、<LinkComponent>低 VRAM ガイド</LinkComponent>に従ってください。",
"noModelsInstalled": "モデルがインストールされていないようです。<DownloadStarterModelsButton>スターターモデルバンドルをダウンロード</DownloadStarterModelsButton>するか、<ImportModelsButton>モデルをインポート</ImportModelsButton>してください。"
},
"whatsNew": {
"whatsNewInInvoke": "Invokeの新機能",
"items": [
"インペインティング: マスクごとのノイズ レベルとノイズ除去の制限。",
"キャンバス: SDXL のアスペクト比がスマートになり、スクロールによるズームが改善されました。"
],
"readReleaseNotes": "リリースノートを読む",
"watchRecentReleaseVideos": "最近のリリースビデオを見る",
"watchUiUpdatesOverview": "Watch UI アップデートの概要"
},
"supportVideos": {
"supportVideos": "サポートビデオ",
"gettingStarted": "はじめる",
"watch": "ウォッチ"
}
}

View File

@@ -74,7 +74,7 @@
"bulkDownloadFailed": "Tải Xuống Thất Bại",
"bulkDownloadRequestFailed": "Có Vấn Đề Khi Đang Chuẩn Bị Tải Xuống",
"download": "Tải Xuống",
"dropOrUpload": "Kéo Thả Hoặc Tải Lên",
"dropOrUpload": "$t(gallery.drop) Hoặc Tải Lên",
"currentlyInUse": "Hình ảnh này hiện đang sử dụng các tính năng sau:",
"deleteImagePermanent": "Ảnh đã xoá không thể phục hồi.",
"exitSearch": "Thoát Tìm Kiếm Hình Ảnh",
@@ -111,7 +111,7 @@
"noImageSelected": "Không Có Ảnh Được Chọn",
"noImagesInGallery": "Không Có Ảnh Để Hiển Thị",
"assetsTab": "Tài liệu bạn đã tải lên để dùng cho dự án của mình.",
"imagesTab": "nh bạn vừa được tạo và lưu trong Invoke.",
"imagesTab": "nh bạn vừa được tạo và lưu trong Invoke.",
"loading": "Đang Tải",
"oldestFirst": "Cũ Nhất Trước",
"exitCompare": "Ngừng So Sánh",
@@ -122,8 +122,7 @@
"boardsSettings": "Thiết Lập Bảng",
"imagesSettings": "Cài Đặt Ảnh Trong Thư Viện Ảnh",
"assets": "Tài Nguyên",
"images": "Hình Ảnh",
"useForPromptGeneration": "Dùng Để Tạo Sinh Lệnh"
"images": "Hình Ảnh"
},
"common": {
"ipAdapter": "IP Adapter",
@@ -255,18 +254,9 @@
"options_withCount_other": "{{count}} thiết lập"
},
"prompt": {
"addPromptTrigger": "Thêm Trigger Cho Lệnh",
"addPromptTrigger": "Thêm Prompt Trigger",
"compatibleEmbeddings": "Embedding Tương Thích",
"noMatchingTriggers": "Không có trigger phù hợp",
"generateFromImage": "Tạo sinh lệnh từ ảnh",
"expandCurrentPrompt": "Mở Rộng Lệnh Hiện Tại",
"uploadImageForPromptGeneration": "Tải Ảnh Để Tạo Sinh Lệnh",
"expandingPrompt": "Đang mở rộng lệnh...",
"resultTitle": "Mở Rộng Lệnh Hoàn Tất",
"resultSubtitle": "Chọn phương thức mở rộng lệnh:",
"replace": "Thay Thế",
"insert": "Chèn",
"discard": "Huỷ Bỏ"
"noMatchingTriggers": "Không có trigger phù hợp"
},
"queue": {
"resume": "Tiếp Tục",
@@ -463,16 +453,6 @@
"applyFilter": {
"title": "Áp Dụng Bộ Lộc",
"desc": "Áp dụng bộ lọc đang chờ sẵn cho layer được chọn."
},
"settings": {
"behavior": "Hành Vi",
"display": "Hiển Thị",
"grid": "Lưới",
"debug": "Gỡ Lỗi"
},
"toggleNonRasterLayers": {
"title": "Bật/Tắt Layer Không Thuộc Dạng Raster",
"desc": "Hiện hoặc ẩn tất cả layer không thuộc dạng raster (Layer Điều Khiển Được, Lớp Phủ Inpaint, Chỉ Dẫn Khu Vực)."
}
},
"workflows": {
@@ -715,7 +695,7 @@
"cancel": "Huỷ",
"huggingFace": "HuggingFace (HF)",
"huggingFacePlaceholder": "chủ-sỡ-hữu/tên-model",
"includesNModels": "Thêm vào {{n}} model và dependency của nó.",
"includesNModels": "Thêm vào {{n}} model và dependency của nó",
"localOnly": "chỉ ở trên máy chủ",
"manual": "Thủ Công",
"convertToDiffusersHelpText4": "Đây là quá trình diễn ra chỉ một lần. Nó có thể tốn tầm 30-60 giây tuỳ theo thông số kỹ thuật của máy tính.",
@@ -762,7 +742,7 @@
"simpleModelPlaceholder": "Url hoặc đường đẫn đến tệp hoặc thư mục chứa diffusers trong máy chủ",
"selectModel": "Chọn Model",
"spandrelImageToImage": "Hình Ảnh Sang Hình Ảnh (Spandrel)",
"starterBundles": "Gói Khởi Đầu",
"starterBundles": "Quà Tân Thủ",
"vae": "VAE",
"urlOrLocalPath": "URL / Đường Dẫn",
"triggerPhrases": "Từ Ngữ Kích Hoạt",
@@ -814,30 +794,7 @@
"manageModels": "Quản Lý Model",
"hfTokenReset": "Làm Mới HF Token",
"relatedModels": "Model Liên Quan",
"showOnlyRelatedModels": "Liên Quan",
"installedModelsCount": "Đã tải {{installed}} trên {{total}} model.",
"allNModelsInstalled": "Đã tải tất cả {{count}} model",
"nToInstall": "Còn {{count}} để tải",
"nAlreadyInstalled": "Có {{count}} đã tải",
"bundleAlreadyInstalled": "Gói đã được cài sẵn",
"bundleAlreadyInstalledDesc": "Tất cả model trong gói {{bundleName}} đã được cài sẵn.",
"launchpadTab": "Launchpad",
"launchpad": {
"welcome": "Chào mừng đến Trình Quản Lý Model",
"description": "Invoke yêu cầu tải model nhằm tối ưu hoá các tính năng trên nền tảng. Chọn tải các phương án thủ công hoặc khám phá các model khởi đầu thích hợp.",
"manualInstall": "Tải Thủ Công",
"urlDescription": "Tải model bằng URL hoặc đường dẫn trên máy. Phù hợp để cụ thể model muốn thêm vào.",
"huggingFaceDescription": "Duyệt và cài đặt model từ các repository trên HuggingFace.",
"scanFolderDescription": "Quét một thư mục trên máy để tự động tra và tải model.",
"recommendedModels": "Model Khuyến Nghị",
"exploreStarter": "Hoặc duyệt tất cả model khởi đầu có sẵn",
"quickStart": "Gói Khởi Đầu Nhanh",
"bundleDescription": "Các gói đều bao gồm những model cần thiết cho từng nhánh model và những model cơ sở đã chọn lọc để bắt đầu.",
"browseAll": "Hoặc duyệt tất cả model có sẵn:",
"stableDiffusion15": "Stable Diffusion 1.5",
"sdxl": "SDXL",
"fluxDev": "FLUX.1 dev"
}
"showOnlyRelatedModels": "Liên Quan"
},
"metadata": {
"guidance": "Hướng Dẫn",
@@ -845,7 +802,7 @@
"imageDetails": "Chi Tiết Ảnh",
"createdBy": "Được Tạo Bởi",
"parsingFailed": "Lỗi Cú Pháp",
"canvasV2Metadata": "Layer Canvas",
"canvasV2Metadata": "Canvas",
"parameterSet": "Dữ liệu tham số {{parameter}}",
"positivePrompt": "Lệnh Tích Cực",
"recallParameter": "Gợi Nhớ {{label}}",
@@ -1517,20 +1474,6 @@
"Lát khối liền mạch bức ảnh theo trục ngang."
],
"heading": "Lát Khối Liền Mạch Trục X"
},
"tileSize": {
"heading": "Kích Thước Khối",
"paragraphs": [
"Điều chỉnh kích thước của khối trong quá trình upscale. Khối càng lớn, bộ nhớ được sử dụng càng nhiều, nhưng có thể tạo sinh ảnh tốt hơn.",
"Model SD1.5 mặt định là 768, trong khi SDXL mặc định là 1024. Giảm kích thước khối nếu các gặp vấn đề bộ nhớ."
]
},
"tileOverlap": {
"heading": "Chồng Chéo Khối",
"paragraphs": [
"Điều chỉnh sự chồng chéo giữa các khối liền kề trong quá trình upscale. Giá trị chồng chép lớn giúp giảm sự rõ nét của các chỗ nối nhau, nhưng ngốn nhiều bộ nhớ hơn.",
"Giá trị mặc định (128) hoạt động tốt với đa số trường hợp, nhưng bạn có thể điều chỉnh cho phù hợp với nhu cầu cụ thể và hạn chế về bộ nhớ."
]
}
},
"models": {
@@ -1544,8 +1487,7 @@
"defaultVAE": "VAE Mặc Định",
"noMatchingModels": "Không có Model phù hợp",
"noModelsAvailable": "Không có model",
"selectModel": "Chọn Model",
"noCompatibleLoRAs": "Không Có LoRAs Tương Thích"
"selectModel": "Chọn Model"
},
"parameters": {
"postProcessing": "Xử Lý Hậu Kỳ (Shift + U)",
@@ -1596,10 +1538,7 @@
"modelIncompatibleBboxHeight": "Chiều dài hộp giới hạn là {{height}} nhưng {{model}} yêu cầu bội số của {{multiple}}",
"modelIncompatibleScaledBboxHeight": "Chiều dài hộp giới hạn theo tỉ lệ là {{height}} nhưng {{model}} yêu cầu bội số của {{multiple}}",
"modelIncompatibleScaledBboxWidth": "Chiều rộng hộp giới hạn theo tỉ lệ là {{width}} nhưng {{model}} yêu cầu bội số của {{multiple}}",
"modelDisabledForTrial": "Tạo sinh với {{modelName}} là không thể với tài khoản trial. Vào phần thiết lập tài khoản để nâng cấp.",
"fluxKontextMultipleReferenceImages": "Chỉ có thể dùng 1 Ảnh Mẫu cùng lúc với Flux Kontext",
"promptExpansionPending": "Trong quá trình mở rộng lệnh",
"promptExpansionResultPending": "Hãy chấp thuận hoặc huỷ bỏ kết quả mở rộng lệnh của bạn"
"modelDisabledForTrial": "Tạo sinh với {{modelName}} là không thể với tài khoản trial. Vào phần thiết lập tài khoản để nâng cấp."
},
"cfgScale": "Thang CFG",
"useSeed": "Dùng Hạt Giống",
@@ -1930,8 +1869,7 @@
"canvasGroup": "Canvas",
"copyCanvasToClipboard": "Sao Chép Canvas Vào Clipboard",
"copyToClipboard": "Sao Chép Vào Clipboard",
"copyBboxToClipboard": "Sao Chép Hộp Giới Hạn Vào Clipboard",
"newResizedControlLayer": "Layer Điều Khiển Được Đã Chỉnh Kích Thước Mới"
"copyBboxToClipboard": "Sao Chép Hộp Giới Hạn Vào Clipboard"
},
"stagingArea": {
"saveToGallery": "Lưu Vào Thư Viện Ảnh",
@@ -2112,11 +2050,7 @@
},
"isolatedLayerPreviewDesc": "Có hay không hiển thị riêng layer này khi thực hiện các thao tác như lọc hay biến đổi.",
"isolatedStagingPreview": "Xem Trước Tổng Quan Phần Cô Lập",
"isolatedPreview": "Xem Trước Phần Cô Lập",
"saveAllImagesToGallery": {
"label": "Chuyển Sản Phẩm Tạo Sinh Mới Vào Thư Viện Ảnh",
"alert": "Đang chuyển sản phẩm tạo sinh mới vào Thư Viện Ảnh, bỏ qua Canvas"
}
"isolatedPreview": "Xem Trước Phần Cô Lập"
},
"tool": {
"eraser": "Tẩy",
@@ -2128,8 +2062,8 @@
"colorPicker": "Chọn Màu"
},
"mergingLayers": "Đang gộp layer",
"controlLayerEmptyState": "<UploadButton>Tải lên ảnh</UploadButton>, kéo thả ảnh từ thư viện ảnh vào layer này, <PullBboxButton>kéo hộp giới hạn vào layer này</PullBboxButton>, hoặc vẽ trên canvas để bắt đầu.",
"referenceImageEmptyState": "<UploadButton>Tải lên hình ảnh</UploadButton> hoặc kéo ảnh từ thư viện ảnh vào Ảnh Mẫu để bắt đầu.",
"controlLayerEmptyState": "<UploadButton>Tải lên ảnh</UploadButton>, kéo thả ảnh từ <GalleryButton>thư viện</GalleryButton> vào layer này, <PullBboxButton>kéo hộp giới hạn vào layer này</PullBboxButton>, hoặc vẽ trên canvas để bắt đầu.",
"referenceImageEmptyState": "<UploadButton>Tải lên hình ảnh</UploadButton>, kéo ảnh từ <GalleryButton>thư viện ảnh</GalleryButton> vào layer này, hoặc <PullBboxButton>kéo hộp giới hạn vào layer này</PullBboxButton> để bắt đầu.",
"useImage": "Dùng Hình Ảnh",
"resetCanvasLayers": "Khởi Động Lại Layer Canvas",
"asRasterLayer": "Như $t(controlLayers.rasterLayer)",
@@ -2181,18 +2115,7 @@
"addDenoiseLimit": "Thêm $t(controlLayers.denoiseLimit)",
"imageNoise": "Độ Nhiễu Hình Ảnh",
"denoiseLimit": "Giới Hạn Khử Nhiễu",
"addImageNoise": "Thêm $t(controlLayers.imageNoise)",
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Tải lên hình ảnh</UploadButton>, kéo ảnh từ thư viện ảnh vào Ảnh Mẫu này, hoặc <PullBboxButton>kéo hộp giới hạn vào Ảnh Mẫu này</PullBboxButton> để bắt đầu.",
"uploadOrDragAnImage": "Kéo ảnh từ thư viện ảnh hoặc <UploadButton>tải lên ảnh</UploadButton>.",
"exportCanvasToPSD": "Xuất Canvas Thành File PSD",
"ruleOfThirds": "Hiển Thị Quy Tắc Một Phần Ba",
"showNonRasterLayers": "Hiển Thị Layer Không Thuộc Dạng Raster (Shift + H)",
"hideNonRasterLayers": "Ẩn Layer Không Thuộc Dạng Raster (Shift + H)",
"autoSwitch": {
"off": "Tắt",
"switchOnStart": "Khi Bắt Đầu",
"switchOnFinish": "Khi Kết Thúc"
}
"addImageNoise": "Thêm $t(controlLayers.imageNoise)"
},
"stylePresets": {
"negativePrompt": "Lệnh Tiêu Cực",
@@ -2238,8 +2161,7 @@
"deleteImage": "Xoá Hình Ảnh",
"exportPromptTemplates": "Xuất Mẫu Trình Bày Cho Lệnh Ra (CSV)",
"templateDeleted": "Mẫu trình bày cho lệnh đã được xoá",
"unableToDeleteTemplate": "Không thể xoá mẫu trình bày cho lệnh",
"togglePromptPreviews": "Bật/Tắt Xem Trước Lệnh"
"unableToDeleteTemplate": "Không thể xoá mẫu trình bày cho lệnh"
},
"system": {
"enableLogging": "Bật Chế Độ Ghi Log",
@@ -2335,26 +2257,7 @@
"workflowUnpublished": "Workflow Đã Được Ngừng Đăng Tải",
"problemUnpublishingWorkflow": "Có Vấn Đề Khi Ngừng Đăng Tải Workflow",
"chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o chỉ hỗ trợ Từ Ngữ Sang Hình Ảnh và Hình Ảnh Sang Hình Ảnh. Hãy dùng model khác cho các tác vụ Inpaint và Outpaint.",
"imagenIncompatibleGenerationMode": "Google {{model}} chỉ hỗ trợ Từ Ngữ Sang Hình Ảnh. Dùng các model khác cho Hình Ảnh Sang Hình Ảnh, Inpaint và Outpaint.",
"fluxKontextIncompatibleGenerationMode": "FLUX Kontext không hỗ trợ tạo sinh từ hình ảnh từ canvas. Thử sử dụng Ảnh Mẫu và tắt các Layer Dạng Raster.",
"noRasterLayers": "Không Tìm Thấy Layer Dạng Raster",
"noRasterLayersDesc": "Tạo ít nhất một layer dạng raster để xuất file PSD",
"noActiveRasterLayers": "Không Có Layer Dạng Raster Hoạt Động",
"noActiveRasterLayersDesc": "Khởi động ít nhất một layer dạng raster để xuất file PSD",
"noVisibleRasterLayers": "Không Có Layer Dạng Raster Hiển Thị",
"noVisibleRasterLayersDesc": "Khởi động ít nhất một layer dạng raster để xuất file PSD",
"invalidCanvasDimensions": "Kích Thước Canvas Không Phù Hợp",
"canvasTooLarge": "Canvas Quá Lớn",
"canvasTooLargeDesc": "Kích thước canvas vượt mức tối đa cho phép để xuất file PSD. Giảm cả chiều dài và chiều rộng chủa canvas và thử lại.",
"failedToProcessLayers": "Thất Bại Khi Xử Lý Layer",
"psdExportSuccess": "Xuất File PSD Hoàn Tất",
"psdExportSuccessDesc": "Thành công xuất {{count}} layer sang file PSD",
"problemExportingPSD": "Có Vấn Đề Khi Xuất File PSD",
"canvasManagerNotAvailable": "Trình Quản Lý Canvas Không Có Sẵn",
"noValidLayerAdapters": "Không có Layer Adaper Phù Hợp",
"promptGenerationStarted": "Trình tạo sinh lệnh khởi động",
"uploadAndPromptGenerationFailed": "Thất bại khi tải lên ảnh để tạo sinh lệnh",
"promptExpansionFailed": "Có vấn đề xảy ra. Hãy thử mở rộng lệnh lại."
"imagenIncompatibleGenerationMode": "Google {{model}} chỉ hỗ trợ Từ Ngữ Sang Hình Ảnh. Dùng các model khác cho Hình Ảnh Sang Hình Ảnh, Inpaint và Outpaint."
},
"ui": {
"tabs": {
@@ -2368,55 +2271,6 @@
"queue": "Queue (Hàng Đợi)",
"workflows": "Workflow (Luồng Làm Việc)",
"workflowsTab": "$t(common.tab) $t(ui.tabs.workflows)"
},
"launchpad": {
"workflowsTitle": "Đi sâu hơn với Workflow.",
"upscalingTitle": "Upscale và thêm chi tiết.",
"canvasTitle": "Biên tập và làm đẹp trên Canvas.",
"generateTitle": "Tạo sinh ảnh từ lệnh chữ.",
"modelGuideText": "Muốn biết lệnh nào tốt nhất cho từng model chứ?",
"modelGuideLink": "Xem thêm Hướng Dẫn Model.",
"workflows": {
"description": "Workflow là các template tái sử dụng được sẽ tự động hoá các tác vụ tạo sinh ảnh, cho phép bạn nhanh chóng thực hiện cách thao tác phức tạp và nhận được kết quả nhất quán.",
"learnMoreLink": "Học thêm cách tạo ra workflow",
"browseTemplates": {
"title": "Duyệt Template Workflow",
"description": "Chọn từ các workflow có sẵn cho những tác vụ cơ bản"
},
"createNew": {
"title": "Tạo workflow mới",
"description": "Tạo workflow mới từ ban đầu"
},
"loadFromFile": {
"title": "Tải workflow từ tệp",
"description": "Tải lên workflow để bắt đầu với những thiết lập sẵn có"
}
},
"upscaling": {
"uploadImage": {
"title": "Tải Ảnh Để Upscale",
"description": "Nhấp hoặc kéo ảnh để upscale (JPG, PNG, WebP lên đến 100MB)"
},
"replaceImage": {
"title": "Thay Thế Ảnh Hiện Tại",
"description": "Nhấp hoặc kéo ảnh mới để thay thế cái hiện tại"
},
"imageReady": {
"title": "Ảnh Đã Sẵn Sàng",
"description": "Bấm 'Kích Hoạt' để chuẩn bị upscale"
},
"readyToUpscale": {
"title": "Chuẩn bị upscale!",
"description": "Điều chỉnh thiết lập bên dưới, sau đó bấm vào nút 'Khởi Động' để chuẩn bị upscale ảnh."
},
"upscaleModel": "Model Upscale",
"model": "Model",
"helpText": {
"promptAdvice": "Khi upscale, dùng lệnh để mô tả phương thức và phong cách. Tránh mô tả các chi tiết cụ thể trong ảnh.",
"styleAdvice": "Upscale thích hợp nhất cho phong cách chung của ảnh."
},
"scale": "Kích Thước"
}
}
},
"workflows": {
@@ -2569,10 +2423,7 @@
"postProcessingMissingModelWarning": "Đến <LinkComponent>Trình Quản Lý Model</LinkComponent> để tải model xử lý hậu kỳ (ảnh sang ảnh).",
"missingModelsWarning": "Đến <LinkComponent>Trình Quản Lý Model</LinkComponent> để tải model cần thiết:",
"incompatibleBaseModel": "Phiên bản model chính không được hỗ trợ để upscale",
"incompatibleBaseModelDesc": "Upscale chỉ hỗ trợ cho model phiên bản SD1.5 và SDXL. Đổi model chính để bật lại tính năng upscale.",
"tileControl": "Điều Chỉnh Khối",
"tileSize": "Kích Thước Khối",
"tileOverlap": "Chồng Chéo Khối"
"incompatibleBaseModelDesc": "Upscale chỉ hỗ trợ cho model phiên bản SD1.5 và SDXL. Đổi model chính để bật lại tính năng upscale."
},
"newUserExperience": {
"toGetStartedLocal": "Để bắt đầu, hãy chắc chắn đã tải xuống hoặc thêm vào model cần để chạy Invoke. Sau đó, nhập lệnh vào hộp và nhấp chuột vào <StrongComponent>Kích Hoạt</StrongComponent> để tạo ra bức ảnh đầu tiên. Chọn một mẫu trình bày cho lệnh để cải thiện kết quả. Bạn có thể chọn để lưu ảnh trực tiếp vào <StrongComponent>Thư Viện Ảnh</StrongComponent> hoặc chỉnh sửa chúng ở <StrongComponent>Canvas</StrongComponent>.",
@@ -2588,9 +2439,8 @@
"watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất",
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
"items": [
"Tạo sinh ảnh nhanh hơn với Launchpad và thẻ Tạo Sinh đã cơ bản hoá.",
"Biên tập với lệnh bằng Flux Kontext Dev.",
"Xuất ra file PSD, ẩn số lượng lớn lớp phủ, sắp xếp model & ảnh — tất cả cho một giao diện đã thiết kế lại để chuyên điều khiển."
"Nvidia 50xx GPUs: Invoke sử dụng PyTorch 2.7.0, thứ tối quan trọng cho những GPU trên.",
"Mối Quan Hệ Model: Kết nối LoRA với model chính, và LoRA đó sẽ được hiển thị đầu danh sách."
]
},
"upsell": {
@@ -2602,18 +2452,64 @@
"supportVideos": {
"supportVideos": "Video Hỗ Trợ",
"gettingStarted": "Bắt Đầu Làm Quen",
"watch": "Xem",
"studioSessionsDesc": "Tham gia <DiscordLink /> để xem các buổi phát trực tiếp và đặt câu hỏi. Các phiên được đăng lên trên playlist các tuần tiếp theo.",
"studioSessionsDesc1": "Xem thử <StudioSessionsPlaylistLink /> để hiểu rõ Invoke hơn.",
"studioSessionsDesc2": "Đến <DiscordLink /> để tham gia vào phiên trực tiếp và hỏi câu hỏi. Các phiên được tải lên danh sách phát vào các tuần.",
"videos": {
"gettingStarted": {
"title": "Bắt Đầu Với Invoke",
"description": "Hoàn thành các video bao hàm mọi thứ bạn cần biết để bắt đầu với Invoke, từ tạo bức ảnh đầu tiên đến các kỹ thuật phức tạp khác."
"howDoIDoImageToImageTransformation": {
"title": "Làm Sao Để Tôi Dùng Trình Biến Đổi Hình Ảnh Sang Hình Ảnh?",
"description": "Hướng dẫn cách thực hiện biến đổi ảnh sang ảnh trong Invoke."
},
"studioSessions": {
"title": "Phiên Studio",
"description": "Đào sâu vào các phiên họp để khám phá những tính năng nâng cao của Invoke, sáng tạo workflow, và thảo luận cộng đồng."
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
"description": "Giới thiệu về ảnh mẫu và IP adapter toàn vùng.",
"title": "Làm Sao Để Tôi Dùng IP Adapter Toàn Vùng Và Ảnh Mẫu?"
},
"creatingAndComposingOnInvokesControlCanvas": {
"description": "Học cách sáng tạo ảnh bằng trình điều khiển canvas của Invoke.",
"title": "Sáng Tạo Trong Trình Kiểm Soát Canvas Của Invoke"
},
"upscaling": {
"description": "Cách upscale ảnh bằng bộ công cụ của Invoke để nâng cấp độ phân giải.",
"title": "Upscale (Nâng Cấp Chất Lượng Hình Ảnh)"
},
"howDoIGenerateAndSaveToTheGallery": {
"title": "Làm Sao Để Tôi Tạo Sinh Và Lưu Vào Thư Viện Ảnh?",
"description": "Các bước để tạo sinh và lưu ảnh vào thư viện ảnh."
},
"howDoIEditOnTheCanvas": {
"description": "Hướng dẫn chỉnh sửa ảnh trực tiếp trên canvas.",
"title": "Làm Sao Để Tôi Chỉnh Sửa Trên Canvas?"
},
"howDoIUseControlNetsAndControlLayers": {
"title": "Làm Sao Để Tôi Dùng ControlNet và Layer Điều Khiển Được?",
"description": "Học cách áp dụng layer điều khiển được và controlnet vào ảnh của bạn."
},
"howDoIUseInpaintMasks": {
"title": "Làm Sao Để Tôi Dùng Lớp Phủ Inpaint?",
"description": "Cách áp dụng lớp phủ inpaint vào chỉnh sửa và thay đổi ảnh."
},
"howDoIOutpaint": {
"title": "Làm Sao Để Tôi Outpaint?",
"description": "Hướng dẫn outpaint bên ngoài viền ảnh gốc."
},
"creatingYourFirstImage": {
"description": "Giới thiệu về cách tạo ảnh từ ban đầu bằng công cụ Invoke.",
"title": "Tạo Hình Ảnh Đầu Tiên Của Bạn"
},
"usingControlLayersAndReferenceGuides": {
"description": "Học cách chỉ dẫn ảnh được tạo ra bằng layer điều khiển được và ảnh mẫu.",
"title": "Dùng Layer Điều Khiển Được và Chỉ Dẫn Mẫu"
},
"understandingImageToImageAndDenoising": {
"title": "Hiểu Rõ Trình Hình Ảnh Sang Hình Ảnh Và Trình Khử Nhiễu",
"description": "Tổng quan về trình biến đổi ảnh sang ảnh và trình khử nhiễu trong Invoke."
},
"exploringAIModelsAndConceptAdapters": {
"title": "Khám Phá Model AI Và Khái Niệm Về Adapter",
"description": "Đào sâu vào model AI và cách dùng những adapter để điều khiển một cách sáng tạo."
}
}
},
"controlCanvas": "Điều Khiển Canvas",
"watch": "Xem"
},
"modelCache": {
"clearSucceeded": "Cache Model Đã Được Dọn",

View File

@@ -31,7 +31,7 @@ import { diff } from 'jsondiffpatch';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
import undoable, { newHistory } from 'redux-undo';
import undoable from 'redux-undo';
import { serializeError } from 'serialize-error';
import { api } from 'services/api';
import { authToastMiddleware } from 'services/api/authToastMiddleware';
@@ -118,7 +118,6 @@ const unserialize: UnserializeFunction = (data, key) => {
if (!persistConfig) {
throw new Error(`No persist config for slice "${key}"`);
}
let state;
try {
const { initialState, migrate } = persistConfig;
const parsed = JSON.parse(data);
@@ -142,21 +141,13 @@ const unserialize: UnserializeFunction = (data, key) => {
},
`Rehydrated slice "${key}"`
);
state = transformed;
return transformed;
} catch (err) {
log.warn(
{ error: serializeError(err as Error) },
`Error rehydrating slice "${key}", falling back to default initial state`
);
state = persistConfig.initialState;
}
// If the slice is undoable, we need to wrap it in a new history - only nodes and canvas are undoable at the moment.
// TODO(psyche): make this automatic & remove the hard-coding for specific slices.
if (key === nodesSlice.name || key === canvasSlice.name) {
return newHistory([], state, []);
} else {
return state;
return persistConfig.initialState;
}
};

View File

@@ -11,13 +11,9 @@ import {
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { typedMemo } from 'common/util/typedMemo';
import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import { selectPickerCompactViewStates } from 'features/ui/store/uiSelectors';
import { pickerCompactViewStateChanged } from 'features/ui/store/uiSlice';
import type { AnyStore, ReadableAtom, Task, WritableAtom } from 'nanostores';
import { atom, computed } from 'nanostores';
import type { StoreValues } from 'nanostores/computed';
@@ -144,10 +140,6 @@ const NoMatchesFallbackWrapper = typedMemo(({ children }: PropsWithChildren) =>
NoMatchesFallbackWrapper.displayName = 'NoMatchesFallbackWrapper';
type PickerProps<T extends object> = {
/**
* Unique identifier for this picker instance. Used to persist compact view state.
*/
pickerId?: string;
/**
* The options to display in the picker. This can be a flat array of options or an array of groups.
*/
@@ -212,18 +204,10 @@ type PickerProps<T extends object> = {
initialGroupStates?: GroupStatusMap;
};
const buildSelectIsCompactView = (pickerId?: string) =>
createSelector([selectPickerCompactViewStates], (compactViewStates) => {
if (!pickerId) {
return true;
}
return compactViewStates[pickerId] ?? true;
});
export type PickerContextState<T extends object> = {
$optionsOrGroups: WritableAtom<OptionOrGroup<T>[]>;
$groupStatusMap: WritableAtom<GroupStatusMap>;
isCompactView: boolean;
$compactView: WritableAtom<boolean>;
$activeOptionId: WritableAtom<string | undefined>;
$filteredOptions: WritableAtom<OptionOrGroup<T>[]>;
$flattenedFilteredOptions: ReadableAtom<T[]>;
@@ -249,7 +233,6 @@ export type PickerContextState<T extends object> = {
OptionComponent: React.ComponentType<{ option: T } & BoxProps>;
NextToSearchBar?: React.ReactNode;
searchable?: boolean;
pickerId?: string;
};
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -520,7 +503,6 @@ const countOptions = <T extends object>(optionsOrGroups: OptionOrGroup<T>[]) =>
export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
const {
pickerId,
getOptionId,
optionsOrGroups,
handleRef,
@@ -539,12 +521,12 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
} = props;
const rootRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { $groupStatusMap, $areAllGroupsDisabled, toggleGroup } = useTogglableGroups(
optionsOrGroups,
initialGroupStates
);
const $activeOptionId = useAtom(getFirstOptionId(optionsOrGroups, getOptionId));
const $compactView = useAtom(true);
const $optionsOrGroups = useAtom(optionsOrGroups);
const $totalOptionCount = useComputed([$optionsOrGroups], countOptions);
const $filteredOptions = useAtom<OptionOrGroup<T>[]>([]);
@@ -556,9 +538,6 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
const $searchTerm = useAtom('');
const $selectedItemId = useComputed([$selectedItem], (item) => (item ? getOptionId(item) : undefined));
const selectIsCompactView = useMemo(() => buildSelectIsCompactView(pickerId), [pickerId]);
const isCompactView = useAppSelector(selectIsCompactView);
const onSelectById = useCallback(
(id: string) => {
const options = $filteredOptions.get();
@@ -586,7 +565,7 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
({
$optionsOrGroups,
$groupStatusMap,
isCompactView,
$compactView,
$activeOptionId,
$filteredOptions,
$flattenedFilteredOptions,
@@ -612,12 +591,11 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
$hasOptions,
$hasFilteredOptions,
$filteredOptionsCount,
pickerId,
}) satisfies PickerContextState<T>,
[
$optionsOrGroups,
$groupStatusMap,
isCompactView,
$compactView,
$activeOptionId,
$filteredOptions,
$flattenedFilteredOptions,
@@ -641,7 +619,6 @@ export const Picker = typedMemo(<T extends object>(props: PickerProps<T>) => {
$hasOptions,
$hasFilteredOptions,
$filteredOptionsCount,
pickerId,
]
);
@@ -892,17 +869,15 @@ GroupToggleButtons.displayName = 'GroupToggleButtons';
const CompactViewToggleButton = typedMemo(<T extends object>() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isCompactView, pickerId } = usePickerContext<T>();
const { $compactView } = usePickerContext<T>();
const compactView = useStore($compactView);
const onClick = useCallback(() => {
if (pickerId) {
dispatch(pickerCompactViewStateChanged({ pickerId, isCompact: !isCompactView }));
}
}, [dispatch, pickerId, isCompactView]);
$compactView.set(!$compactView.get());
}, [$compactView]);
const label = isCompactView ? t('common.fullView') : t('common.compactView');
const icon = isCompactView ? <PiArrowsOutLineVerticalBold /> : <PiArrowsInLineVerticalBold />;
const label = compactView ? t('common.fullView') : t('common.compactView');
const icon = compactView ? <PiArrowsOutLineVerticalBold /> : <PiArrowsInLineVerticalBold />;
return <IconButton aria-label={label} tooltip={label} size="sm" variant="ghost" icon={icon} onClick={onClick} />;
});
@@ -949,7 +924,8 @@ const listSx = {
} satisfies SystemStyleObject;
const PickerList = typedMemo(<T extends object>() => {
const { getOptionId, isCompactView, $filteredOptions } = usePickerContext<T>();
const { getOptionId, $compactView, $filteredOptions } = usePickerContext<T>();
const compactView = useStore($compactView);
const filteredOptions = useStore($filteredOptions);
if (filteredOptions.length === 0) {
@@ -958,10 +934,10 @@ const PickerList = typedMemo(<T extends object>() => {
return (
<ScrollableContent>
<Flex sx={listSx} data-is-compact={isCompactView}>
<Flex sx={listSx} data-is-compact={compactView}>
{filteredOptions.map((optionOrGroup, i) => {
if (isGroup(optionOrGroup)) {
const withDivider = !isCompactView && i < filteredOptions.length - 1;
const withDivider = !compactView && i < filteredOptions.length - 1;
return (
<React.Fragment key={optionOrGroup.id}>
<PickerGroup group={optionOrGroup} />
@@ -1103,13 +1079,14 @@ const groupHeaderSx = {
const PickerGroupHeader = typedMemo(<T extends object>({ group }: { group: Group<T> }) => {
const { t } = useTranslation();
const { isCompactView } = usePickerContext<T>();
const { $compactView } = usePickerContext<T>();
const compactView = useStore($compactView);
const color = getGroupColor(group);
const name = getGroupName(group);
const count = getGroupCount(group, t);
return (
<Flex sx={groupHeaderSx} data-is-compact={isCompactView}>
<Flex sx={groupHeaderSx} data-is-compact={compactView}>
<Flex gap={2} alignItems="center">
<Text fontSize="sm" fontWeight="semibold" color={color} noOfLines={1}>
{name}

View File

@@ -1,8 +1,6 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { canvasReset } from 'features/controlLayers/store/actions';
import { inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { allEntitiesDeleted } from 'features/controlLayers/store/canvasSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,9 +11,7 @@ export const SessionMenuItems = memo(() => {
const dispatch = useAppDispatch();
const resetCanvasLayers = useCallback(() => {
dispatch(canvasReset());
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
$canvasManager.get()?.stage.fitBboxToStage();
dispatch(allEntitiesDeleted());
}, [dispatch]);
const resetGenerationSettings = useCallback(() => {
dispatch(paramsReset());

View File

@@ -139,13 +139,4 @@ export const useGlobalHotkeys = () => {
},
dependencies: [getState, deleteImageModalApi],
});
useRegisteredHotkeys({
id: 'toggleViewer',
category: 'viewer',
callback: () => {
navigationApi.toggleViewerPanel();
},
dependencies: [],
});
};

View File

@@ -165,9 +165,9 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
<Spacer />
</Flex>
{type === 'raster_layer' && <RasterLayerExportPSDButton />}
<CanvasEntityMergeVisibleButton type={type} />
<CanvasEntityTypeIsHiddenToggle type={type} />
{type === 'raster_layer' && <RasterLayerExportPSDButton />}
<CanvasEntityAddOfTypeButton type={type} />
</Flex>
<Collapse in={collapse.isTrue} style={fixTooltipCloseOnScrollStyles}>

View File

@@ -3,7 +3,6 @@ import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/co
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton';
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
@@ -22,7 +21,6 @@ export const EntityListSelectedEntityActionBar = memo(() => {
<EntityListSelectedEntityActionBarSelectObjectButton />
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarInvertMaskButton />
<EntityListSelectedEntityActionBarSaveToAssetsButton />
<EntityListSelectedEntityActionBarDuplicateButton />
<EntityListNonRasterLayerToggle />

View File

@@ -1,39 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useInvertMask } from 'features/controlLayers/hooks/useInvertMask';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isInpaintMaskEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSelectionInverseBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarInvertMaskButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const invertMask = useInvertMask();
if (!selectedEntityIdentifier) {
return null;
}
if (!isInpaintMaskEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={invertMask}
isDisabled={isBusy}
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.invertMask')}
tooltip={t('controlLayers.invertMask')}
icon={<PiSelectionInverseBold />}
/>
);
});
EntityListSelectedEntityActionBarInvertMaskButton.displayName = 'EntityListSelectedEntityActionBarInvertMaskButton';

View File

@@ -62,10 +62,6 @@ export const QueueItemPreviewMini = memo(({ item, index }: Props) => {
}
}, [autoSwitch, dispatch]);
const onLoad = useCallback(() => {
ctx.onImageLoaded(item.item_id);
}, [ctx, item.item_id]);
return (
<Flex
id={getQueueItemElementId(index)}
@@ -75,7 +71,7 @@ export const QueueItemPreviewMini = memo(({ item, index }: Props) => {
onDoubleClick={onDoubleClick}
>
<QueueItemStatusLabel item={item} position="absolute" margin="auto" />
{imageDTO && <DndImage imageDTO={imageDTO} position="absolute" onLoad={onLoad} />}
{imageDTO && <DndImage imageDTO={imageDTO} asThumbnail position="absolute" />}
<QueueItemProgressImage itemId={item.item_id} position="absolute" />
<QueueItemNumber number={index + 1} position="absolute" top={0} left={1} />
<QueueItemCircularProgress itemId={item.item_id} status={item.status} position="absolute" top={1} right={2} />

View File

@@ -7,9 +7,9 @@ import { useProgressDatum } from './context';
type Props = { itemId: number } & ImageProps;
export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
const { progressImage, imageLoaded } = useProgressDatum(itemId);
const { progressImage } = useProgressDatum(itemId);
if (!progressImage || imageLoaded) {
if (!progressImage) {
return null;
}

View File

@@ -12,6 +12,7 @@ export const createMockStagingAreaApp = (): StagingAreaAppApi & {
_triggerInvocationProgress: (data: S['InvocationProgressEvent']) => void;
_setAutoSwitchMode: (mode: AutoSwitchMode) => void;
_setImageDTO: (imageName: string, imageDTO: ImageDTO | null) => void;
_setLoadImageDelay: (delay: number) => void;
} => {
const itemsChangedHandlers = new Set<(items: S['SessionQueueItem'][]) => void>();
const queueItemStatusChangedHandlers = new Set<(data: S['QueueItemStatusChangedEvent']) => void>();
@@ -19,6 +20,7 @@ export const createMockStagingAreaApp = (): StagingAreaAppApi & {
let autoSwitchMode: AutoSwitchMode = 'switch_on_start';
const imageDTOs = new Map<string, ImageDTO | null>();
let loadImageDelay = 0;
return {
onDiscard: vi.fn(),
@@ -34,6 +36,22 @@ export const createMockStagingAreaApp = (): StagingAreaAppApi & {
getImageDTO: vi.fn((imageName: string) => {
return Promise.resolve(imageDTOs.get(imageName) || null);
}),
loadImage: vi.fn(async (imageName: string) => {
if (loadImageDelay > 0) {
await new Promise((resolve) => {
setTimeout(resolve, loadImageDelay);
});
}
// Mock HTMLImageElement for testing environment
const mockImage = {
src: imageName,
width: 512,
height: 512,
onload: null,
onerror: null,
} as HTMLImageElement;
return mockImage;
}),
onItemsChanged: vi.fn((handler) => {
itemsChangedHandlers.add(handler);
return () => itemsChangedHandlers.delete(handler);
@@ -63,6 +81,9 @@ export const createMockStagingAreaApp = (): StagingAreaAppApi & {
_setImageDTO: (imageName: string, imageDTO: ImageDTO | null) => {
imageDTOs.set(imageName, imageDTO);
},
_setLoadImageDelay: (delay: number) => {
loadImageDelay = delay;
},
};
};

View File

@@ -1,5 +1,6 @@
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { loadImage } from 'features/controlLayers/konva/util';
import {
selectStagingAreaAutoSwitch,
settingsStagingAreaAutoSwitchChanged,
@@ -35,6 +36,7 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
const _stagingAreaAppApi: StagingAreaAppApi = {
getAutoSwitch: () => selectStagingAreaAutoSwitch(store.getState()),
getImageDTO: (imageName: string) => getImageDTOSafe(imageName),
loadImage: (imageUrl: string) => loadImage(imageUrl, true),
onInvocationProgress: (handler) => {
socket?.on('invocation_progress', handler);
return () => {

View File

@@ -96,7 +96,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO,
imageLoaded: false,
});
expect(api.$selectedItemImageDTO.get()).toBe(imageDTO);
@@ -263,7 +262,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO,
imageLoaded: false,
});
const selectedItem = api.$selectedItem.get();
@@ -278,7 +276,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO: null,
imageLoaded: false,
});
api.acceptSelected();
@@ -302,7 +299,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO,
imageLoaded: false,
});
expect(api.$acceptSelectedIsEnabled.get()).toBe(true);
@@ -340,7 +336,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO: createMockImageDTO(),
imageLoaded: false,
});
const progressEvent = createMockProgressEvent({
@@ -460,7 +455,6 @@ describe('StagingAreaApi', () => {
// Wait for async image loading - the loadImage promise needs to complete
await new Promise((resolve) => {
setTimeout(resolve, 50);
api.onImageLoaded(1);
});
expect(api.$selectedItemId.get()).toBe(1);
@@ -474,7 +468,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO: null,
imageLoaded: false,
});
const items = [createMockQueueItem({ item_id: 1 })];
@@ -491,7 +484,6 @@ describe('StagingAreaApi', () => {
progressEvent: createMockProgressEvent({ item_id: 1 }),
progressImage: null,
imageDTO: createMockImageDTO(),
imageLoaded: false,
});
const items = [createMockQueueItem({ item_id: 1, status: 'canceled' })];
@@ -505,45 +497,11 @@ describe('StagingAreaApi', () => {
});
});
describe('Image Loading', () => {
it('should handle image loading for completed items', () => {
api.$items.set([createMockQueueItem({ item_id: 1 })]);
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
mockApp._setImageDTO('test-image.png', imageDTO);
api.onImageLoaded(1);
const progressData = api.$progressData.get();
expect(progressData[1]?.imageLoaded).toBe(true);
});
});
describe('Auto Switch', () => {
it('should set auto switch mode', () => {
api.setAutoSwitch('switch_on_finish');
expect(mockApp.onAutoSwitchChange).toHaveBeenCalledWith('switch_on_finish');
});
it('should auto-switch on finish when the image loads', async () => {
mockApp._setAutoSwitchMode('switch_on_finish');
api.$lastCompletedItemId.set(1);
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
mockApp._setImageDTO('test-image.png', imageDTO);
const items = [
createMockQueueItem({
item_id: 1,
status: 'completed',
}),
];
await api.onItemsChangedEvent(items);
await new Promise((resolve) => {
setTimeout(resolve, 50);
api.onImageLoaded(1);
});
expect(api.$selectedItemId.get()).toBe(1);
expect(api.$lastCompletedItemId.get()).toBe(null);
});
});
describe('Utility Methods', () => {
@@ -567,7 +525,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO: null,
imageLoaded: false,
});
api.cleanup();
@@ -629,7 +586,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO,
imageLoaded: false,
});
const progressEvent = createMockProgressEvent({
@@ -662,6 +618,9 @@ describe('StagingAreaApi', () => {
mockApp._setAutoSwitchMode('switch_on_finish');
api.$lastCompletedItemId.set(1);
// Mock image loading failure
mockApp._setImageDTO('test-image.png', null);
const items = [
createMockQueueItem({
item_id: 1,
@@ -683,14 +642,41 @@ describe('StagingAreaApi', () => {
await api.onItemsChangedEvent(items);
// Wait a while but do not load the image
// Should not switch when image loading fails
expect(api.$selectedItemId.get()).toBe(1);
expect(api.$lastCompletedItemId.get()).toBe(1);
});
it('should handle auto-switch on finish with slow image loading', async () => {
mockApp._setAutoSwitchMode('switch_on_finish');
api.$lastCompletedItemId.set(1);
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
mockApp._setImageDTO('test-image.png', imageDTO);
mockApp._setLoadImageDelay(50); // Add delay to image loading
const items = [
createMockQueueItem({
item_id: 1,
status: 'completed',
session: {
id: sessionId,
source_prepared_mapping: { canvas_output: ['test-node-id'] },
results: { 'test-node-id': { image: { image_name: 'test-image.png' } } },
},
}),
];
await api.onItemsChangedEvent(items);
// Should switch after image loads - wait for both the delay and promise resolution
await new Promise((resolve) => {
setTimeout(resolve, 150);
});
// Should not switch when image loading fails
expect(api.$selectedItemId.get()).toBe(1);
expect(api.$lastCompletedItemId.get()).toBe(1);
// The lastCompletedItemId should be reset after the loadImage promise resolves
expect(api.$lastCompletedItemId.get()).toBe(null);
});
});
@@ -742,7 +728,6 @@ describe('StagingAreaApi', () => {
progressEvent: null,
progressImage: null,
imageDTO: null,
imageLoaded: false,
});
}

View File

@@ -24,6 +24,7 @@ export type StagingAreaAppApi = {
getAutoSwitch: () => AutoSwitchMode;
onAutoSwitchChange?: (mode: AutoSwitchMode) => void;
getImageDTO: (imageName: string) => Promise<ImageDTO | null>;
loadImage: (imageName: string) => Promise<HTMLImageElement>;
onItemsChanged: (handler: (data: S['SessionQueueItem'][]) => Promise<void> | void) => () => void;
onQueueItemStatusChanged: (handler: (data: S['QueueItemStatusChangedEvent']) => Promise<void> | void) => () => void;
onInvocationProgress: (handler: (data: S['InvocationProgressEvent']) => Promise<void> | void) => () => void;
@@ -35,7 +36,6 @@ export type ProgressData = {
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
imageDTO: ImageDTO | null;
imageLoaded: boolean;
};
/** Combined data for the currently selected item */
@@ -51,7 +51,6 @@ export const getInitialProgressData = (itemId: number): ProgressData => ({
progressEvent: null,
progressImage: null,
imageDTO: null,
imageLoaded: false,
});
type ProgressDataMap = Record<number, ProgressData | undefined>;
@@ -283,7 +282,7 @@ export class StagingAreaApi {
* queue items as part of the list query, so it's rather inefficient to fetch it again here.
*
* To reduce the number of extra network requests, we instead store this item as the last completed item.
* Then when the image loads, it calls onImageLoaded and we switch to it then.
* Then in the progress data sync effect, we process the queue item load its image.
*/
this.$lastCompletedItemId.set(data.item_id);
}
@@ -313,74 +312,73 @@ export class StagingAreaApi {
const progressData = this.$progressData.get();
const toDelete: number[] = [];
const toUpdate: ProgressData[] = [];
for (const [id, datum] of objectEntries(progressData)) {
if (!datum || !items.find(({ item_id }) => item_id === datum.itemId)) {
this.$progressData.setKey(id, undefined);
if (!datum) {
toDelete.push(id);
continue;
}
const item = items.find(({ item_id }) => item_id === datum.itemId);
if (!item) {
toDelete.push(datum.itemId);
} else if (item.status === 'canceled' || item.status === 'failed') {
toUpdate.push({
...datum,
progressEvent: null,
progressImage: null,
imageDTO: null,
});
}
}
for (const item of items) {
const datum = progressData[item.item_id];
if (item.status === 'canceled' || item.status === 'failed') {
this.$progressData.setKey(item.item_id, {
...(datum ?? getInitialProgressData(item.item_id)),
progressEvent: null,
progressImage: null,
imageDTO: null,
});
if (this.$lastStartedItemId.get() === item.item_id && this._app?.getAutoSwitch() === 'switch_on_start') {
this.$selectedItemId.set(item.item_id);
this.$lastStartedItemId.set(null);
}
if (datum?.imageDTO) {
continue;
}
const outputImageName = getOutputImageName(item);
if (!outputImageName) {
continue;
}
const imageDTO = await this._app?.getImageDTO(outputImageName);
if (!imageDTO) {
continue;
}
if (item.status === 'in_progress') {
if (this.$lastStartedItemId.get() === item.item_id && this._app?.getAutoSwitch() === 'switch_on_start') {
// This is the load logic mentioned in the comment in the QueueItemStatusChangedEvent handler above.
if (this.$lastCompletedItemId.get() === item.item_id && this._app?.getAutoSwitch() === 'switch_on_finish') {
this._app.loadImage(imageDTO.image_url).then(() => {
this.$selectedItemId.set(item.item_id);
this.$lastStartedItemId.set(null);
}
continue;
}
if (item.status === 'completed') {
if (datum?.imageDTO) {
continue;
}
const outputImageName = getOutputImageName(item);
if (!outputImageName) {
continue;
}
const imageDTO = await this._app?.getImageDTO(outputImageName);
if (!imageDTO) {
continue;
}
this.$progressData.setKey(item.item_id, {
...(datum ?? getInitialProgressData(item.item_id)),
imageDTO,
this.$lastCompletedItemId.set(null);
});
}
toUpdate.push({
...getInitialProgressData(item.item_id),
...datum,
imageDTO,
});
}
for (const itemId of toDelete) {
this.$progressData.setKey(itemId, undefined);
}
for (const datum of toUpdate) {
this.$progressData.setKey(datum.itemId, datum);
}
this.$items.set(items);
};
onImageLoaded = (itemId: number) => {
const item = this.$items.get().find(({ item_id }) => item_id === itemId);
if (!item) {
return;
}
// This is the load logic mentioned in the comment in the QueueItemStatusChangedEvent handler above.
if (this.$lastCompletedItemId.get() === item.item_id && this._app?.getAutoSwitch() === 'switch_on_finish') {
this.$selectedItemId.set(item.item_id);
this.$lastCompletedItemId.set(null);
}
const datum = this.$progressData.get()[item.item_id];
this.$progressData.setKey(item.item_id, {
...(datum ?? getInitialProgressData(item.item_id)),
imageLoaded: true,
});
};
/** Creates a computed value that returns true if the given item ID is selected. */
buildIsSelectedComputed = (itemId: number) => {
return computed([this.$selectedItemId], (selectedItemId) => {
@@ -404,13 +402,25 @@ export class StagingAreaApi {
const setProgress = ($progressData: MapStore<ProgressDataMap>, data: S['InvocationProgressEvent']) => {
const progressData = $progressData.get();
const current = progressData[data.item_id];
const next = { ...(current ?? getInitialProgressData(data.item_id)) };
next.progressEvent = data;
if (data.image) {
next.progressImage = data.image;
if (current) {
const next = { ...current };
next.progressEvent = data;
if (data.image) {
next.progressImage = data.image;
}
$progressData.set({
...progressData,
[data.item_id]: next,
});
} else {
$progressData.set({
...progressData,
[data.item_id]: {
itemId: data.item_id,
progressEvent: data,
progressImage: data.image ?? null,
imageDTO: null,
},
});
}
$progressData.set({
...progressData,
[data.item_id]: next,
});
};

View File

@@ -3,7 +3,6 @@ import { CanvasSettingsPopover } from 'features/controlLayers/components/Setting
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
import { CanvasToolbarFitBboxToMasksButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToMasksButton';
import { CanvasToolbarNewSessionMenuButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarNewSessionMenuButton';
import { CanvasToolbarRedoButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarRedoButton';
import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton';
@@ -13,7 +12,6 @@ import { CanvasToolbarUndoButton } from 'features/controlLayers/components/Toolb
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey';
import { useCanvasFilterHotkey } from 'features/controlLayers/hooks/useCanvasFilterHotkey';
import { useCanvasInvertMaskHotkey } from 'features/controlLayers/hooks/useCanvasInvertMaskHotkey';
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
import { useCanvasToggleNonRasterLayersHotkey } from 'features/controlLayers/hooks/useCanvasToggleNonRasterLayersHotkey';
import { useCanvasTransformHotkey } from 'features/controlLayers/hooks/useCanvasTransformHotkey';
@@ -29,7 +27,6 @@ export const CanvasToolbar = memo(() => {
useNextPrevEntityHotkeys();
useCanvasTransformHotkey();
useCanvasFilterHotkey();
useCanvasInvertMaskHotkey();
useCanvasToggleNonRasterLayersHotkey();
return (
@@ -40,7 +37,6 @@ export const CanvasToolbar = memo(() => {
<CanvasToolbarScale />
<CanvasToolbarResetViewButton />
<CanvasToolbarFitBboxToLayersButton />
<CanvasToolbarFitBboxToMasksButton />
</Flex>
<Divider orientation="vertical" />
<Flex alignItems="center" h="full">

View File

@@ -1,45 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAutoFitBBoxToMasks } from 'features/controlLayers/hooks/useAutoFitBBoxToMasks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useVisibleEntityCountByType } from 'features/controlLayers/hooks/useVisibleEntityCountByType';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSelectionAllDuotone } from 'react-icons/pi';
export const CanvasToolbarFitBboxToMasksButton = memo(() => {
const { t } = useTranslation();
const isBusy = useCanvasIsBusy();
const fitBBoxToMasks = useAutoFitBBoxToMasks();
// Check if there are any visible inpaint masks
const visibleMaskCount = useVisibleEntityCountByType('inpaint_mask');
const hasVisibleMasks = visibleMaskCount > 0;
const onClick = useCallback(() => {
fitBBoxToMasks();
}, [fitBBoxToMasks]);
// Register hotkey for Shift+B
useRegisteredHotkeys({
id: 'fitBboxToMasks',
category: 'canvas',
callback: fitBBoxToMasks,
options: { enabled: !isBusy && hasVisibleMasks },
dependencies: [fitBBoxToMasks, isBusy, hasVisibleMasks],
});
return (
<IconButton
onClick={onClick}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.fitBboxToMasks')}
tooltip={t('controlLayers.fitBboxToMasks')}
icon={<PiSelectionAllDuotone />}
isDisabled={isBusy || !hasVisibleMasks}
/>
);
});
CanvasToolbarFitBboxToMasksButton.displayName = 'CanvasToolbarFitBboxToMasksButton';

View File

@@ -1,40 +0,0 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { fitRectToGrid } from 'features/controlLayers/konva/util';
import { selectMaskBlur } from 'features/controlLayers/store/paramsSlice';
import { useCallback } from 'react';
export const useAutoFitBBoxToMasks = () => {
const canvasManager = useCanvasManager();
const maskBlur = useAppSelector(selectMaskBlur);
const fitBBoxToMasks = useCallback(() => {
// Get the rect of all visible inpaint masks
const visibleRect = canvasManager.compositor.getVisibleRectOfType('inpaint_mask');
// Can't fit the bbox to nothing
if (visibleRect.height === 0 || visibleRect.width === 0) {
return;
}
// Account for mask blur expansion and add 8px padding
const padding = 8;
const totalPadding = maskBlur + padding;
const expandedRect = {
x: visibleRect.x - totalPadding,
y: visibleRect.y - totalPadding,
width: visibleRect.width + totalPadding * 2,
height: visibleRect.height + totalPadding * 2,
};
// Apply grid fitting using the bbox grid size
const gridSize = canvasManager.stateApi.getBboxGridSize();
const rect = fitRectToGrid(expandedRect, gridSize);
// Update the generation bbox
canvasManager.stateApi.setGenerationBbox(rect);
}, [canvasManager, maskBlur]);
return fitBBoxToMasks;
};

View File

@@ -1,36 +0,0 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useInvertMask } from 'features/controlLayers/hooks/useInvertMask';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isInpaintMaskEntityIdentifier } from 'features/controlLayers/store/types';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useMemo } from 'react';
export const useCanvasInvertMaskHotkey = () => {
useAssertSingleton('useCanvasInvertMaskHotkey');
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const invertMask = useInvertMask();
const isEnabled = useMemo(() => {
if (!selectedEntityIdentifier) {
return false;
}
if (!isInpaintMaskEntityIdentifier(selectedEntityIdentifier)) {
return false;
}
if (isBusy) {
return false;
}
return true;
}, [selectedEntityIdentifier, isBusy]);
useRegisteredHotkeys({
id: 'invertMask',
category: 'canvas',
callback: invertMask,
options: { enabled: isEnabled, preventDefault: true },
dependencies: [invertMask, isEnabled],
});
};

View File

@@ -1,103 +0,0 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { canvasToBlob, canvasToImageData } from 'features/controlLayers/konva/util';
import { entityRasterized } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { uploadImage } from 'services/api/endpoints/images';
export const useInvertMask = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const invertMask = useCallback(async () => {
try {
const bboxRect = canvasManager.stateApi.getBbox().rect;
const adapters = canvasManager.compositor.getVisibleAdaptersOfType('inpaint_mask');
if (adapters.length === 0) {
toast({
id: 'NO_VISIBLE_MASKS',
title: t('toast.noVisibleMasks'),
description: t('toast.noVisibleMasksDesc'),
status: 'warning',
});
return;
}
const fullCanvas = document.createElement('canvas');
fullCanvas.width = bboxRect.width;
fullCanvas.height = bboxRect.height;
const fullCtx = fullCanvas.getContext('2d');
if (!fullCtx) {
throw new Error('Failed to get canvas context');
}
fullCtx.fillStyle = 'rgba(0, 0, 0, 0)';
fullCtx.fillRect(0, 0, bboxRect.width, bboxRect.height);
const visibleMasksRect = canvasManager.compositor.getVisibleRectOfType('inpaint_mask');
if (visibleMasksRect.width > 0 && visibleMasksRect.height > 0) {
const compositeCanvas = canvasManager.compositor.getCompositeCanvas(adapters, visibleMasksRect);
const offsetX = visibleMasksRect.x - bboxRect.x;
const offsetY = visibleMasksRect.y - bboxRect.y;
fullCtx.drawImage(compositeCanvas, offsetX, offsetY);
}
const imageData = canvasToImageData(fullCanvas);
const data = imageData.data;
for (let i = 3; i < data.length; i += 4) {
data[i] = 255 - (data[i] ?? 0); // Invert alpha
}
fullCtx.putImageData(imageData, 0, 0);
const blob = await canvasToBlob(fullCanvas);
const imageDTO = await uploadImage({
file: new File([blob], 'inverted-mask.png', { type: 'image/png' }),
image_category: 'general',
is_intermediate: true,
silent: true,
});
const imageObject = imageDTOToImageObject(imageDTO);
if (selectedEntityIdentifier) {
dispatch(
entityRasterized({
entityIdentifier: selectedEntityIdentifier,
imageObject,
position: { x: bboxRect.x, y: bboxRect.y },
replaceObjects: true,
isSelected: true,
})
);
}
toast({
id: 'MASK_INVERTED',
title: t('toast.maskInverted'),
status: 'success',
});
} catch (error) {
toast({
id: 'MASK_INVERT_FAILED',
title: t('toast.maskInvertFailed'),
description: String(error),
status: 'error',
});
}
}, [canvasManager, dispatch, selectedEntityIdentifier, t]);
return invertMask;
};

View File

@@ -42,7 +42,7 @@ const DEFAULT_CONFIG: CanvasStageModuleConfig = {
SCALE_FACTOR: 0.999,
FIT_LAYERS_TO_STAGE_PADDING_PX: 48,
SCALE_SNAP_POINTS: [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5],
SCALE_SNAP_TOLERANCE: 0.02,
SCALE_SNAP_TOLERANCE: 0.05,
};
export class CanvasStageModule extends CanvasModuleBase {
@@ -366,22 +366,11 @@ export class CanvasStageModule extends CanvasModuleBase {
if (deltaT > 300) {
dynamicScaleFactor = this.config.SCALE_FACTOR + (1 - this.config.SCALE_FACTOR) / 2;
} else if (deltaT < 300) {
// Ensure dynamic scale factor stays below 1 to maintain zoom-out direction - if it goes over, we could end up
// zooming in the wrong direction with small scroll amounts
const maxScaleFactor = 0.9999;
dynamicScaleFactor = Math.min(
this.config.SCALE_FACTOR + (1 - this.config.SCALE_FACTOR) * (deltaT / 200),
maxScaleFactor
);
dynamicScaleFactor = this.config.SCALE_FACTOR + (1 - this.config.SCALE_FACTOR) * (deltaT / 200);
}
// Update the intended scale based on the last intended scale, creating a continuous zoom feel
// Handle the sign explicitly to prevent direction reversal with small scroll amounts
const scaleFactor =
scrollAmount > 0
? dynamicScaleFactor ** Math.abs(scrollAmount)
: (1 / dynamicScaleFactor) ** Math.abs(scrollAmount);
const newIntendedScale = this._intendedScale * scaleFactor;
const newIntendedScale = this._intendedScale * dynamicScaleFactor ** scrollAmount;
this._intendedScale = this.constrainScale(newIntendedScale);
// Pass control to the snapping logic
@@ -408,9 +397,6 @@ export class CanvasStageModule extends CanvasModuleBase {
// User has scrolled far enough to break the snap
this._activeSnapPoint = null;
this._applyScale(this._intendedScale, center);
} else {
// Reset intended scale to prevent drift while snapped
this._intendedScale = this._activeSnapPoint;
}
// Else, do nothing - we remain snapped at the current scale, creating a "dead zone"
return;

View File

@@ -1618,6 +1618,7 @@ export const {
entityArrangedToBack,
entityOpacityChanged,
entitiesReordered,
allEntitiesDeleted,
allEntitiesOfTypeIsHiddenToggled,
allNonRasterLayersIsHiddenToggled,
// bbox

View File

@@ -21,14 +21,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
const { dispatch, getState } = store;
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
await newCanvasFromImage({
imageDTO,
withResize: false,
withInpaintMask: true,
type: 'raster_layer',
dispatch,
getState,
});
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
@@ -39,14 +32,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
const { dispatch, getState } = store;
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
await newCanvasFromImage({
imageDTO,
withResize: false,
withInpaintMask: true,
type: 'control_layer',
dispatch,
getState,
});
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
@@ -57,14 +43,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
const { dispatch, getState } = store;
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
await newCanvasFromImage({
imageDTO,
withResize: true,
withInpaintMask: true,
type: 'raster_layer',
dispatch,
getState,
});
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
@@ -75,14 +54,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
const { dispatch, getState } = store;
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
await newCanvasFromImage({
imageDTO,
withResize: true,
withInpaintMask: true,
type: 'control_layer',
dispatch,
getState,
});
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),

View File

@@ -2,14 +2,13 @@ import { Box, Flex, forwardRef, Grid, GridItem, Spinner, Text } from '@invoke-ai
import { createSelector } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { getFocusedRegion, useIsRegionFocused } from 'common/hooks/focus';
import { getFocusedRegion } from 'common/hooks/focus';
import { useRangeBasedImageFetching } from 'features/gallery/hooks/useRangeBasedImageFetching';
import type { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
import {
selectGalleryImageMinimumWidth,
selectImageToCompare,
selectLastSelectedImage,
selectSelectionCount,
} from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
@@ -25,7 +24,7 @@ import type {
VirtuosoGridHandle,
} from 'react-virtuoso';
import { VirtuosoGrid } from 'react-virtuoso';
import { imagesApi, useImageDTO, useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
import { imagesApi } from 'services/api/endpoints/images';
import { useDebounce } from 'use-debounce';
import { GalleryImage, GalleryImagePlaceholder } from './ImageGrid/GalleryImage';
@@ -451,37 +450,6 @@ const useScrollableGallery = (rootRef: RefObject<HTMLDivElement>) => {
return scrollerRef;
};
const useStarImageHotkey = () => {
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const selectionCount = useAppSelector(selectSelectionCount);
const isGalleryFocused = useIsRegionFocused('gallery');
const imageDTO = useImageDTO(lastSelectedImage);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
const handleStarHotkey = useCallback(() => {
if (!imageDTO) {
return;
}
if (!isGalleryFocused) {
return;
}
if (imageDTO.starred) {
unstarImages({ image_names: [imageDTO.image_name] });
} else {
starImages({ image_names: [imageDTO.image_name] });
}
}, [imageDTO, isGalleryFocused, starImages, unstarImages]);
useRegisteredHotkeys({
id: 'starImage',
category: 'gallery',
callback: handleStarHotkey,
options: { enabled: !!imageDTO && selectionCount === 1 && isGalleryFocused },
dependencies: [imageDTO, selectionCount, isGalleryFocused, handleStarHotkey],
});
};
export const NewGallery = memo(() => {
const virtuosoRef = useRef<VirtuosoGridHandle>(null);
const rangeRef = useRef<ListRange>({ startIndex: 0, endIndex: 0 });
@@ -496,7 +464,6 @@ export const NewGallery = memo(() => {
enabled: !isLoading,
});
useStarImageHotkey();
useKeepSelectedImageInView(imageNames, virtuosoRef, rootRef, rangeRef);
useKeyboardNavigation(imageNames, virtuosoRef, rootRef);
const scrollerRef = useScrollableGallery(rootRef);

View File

@@ -82,7 +82,6 @@ const LoRASelect = () => {
<FormLabel>{t('models.concepts')} </FormLabel>
</InformationalPopover>
<ModelPicker
pickerId="lora-select"
modelConfigs={compatibleLoRAs}
onChange={onChange}
grouped={false}

View File

@@ -263,7 +263,7 @@ export const Flow = memo(() => {
noWheelClassName={NO_WHEEL_CLASS}
noPanClassName={NO_PAN_CLASS}
>
<Background gap={snapGrid} offset={snapGrid} />
<Background />
</ReactFlow>
<HotkeyIsolator />
</>

View File

@@ -6,7 +6,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { DndImage } from 'features/dnd/DndImage';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import NonInvocationNodeWrapper from 'features/nodes/components/flow/nodes/common/NonInvocationNodeWrapper';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
@@ -58,14 +58,13 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
}, []);
const { t } = useTranslation();
return (
<NonInvocationNodeWrapper nodeId={props.nodeProps.id} selected={props.nodeProps.selected} width={384}>
<NodeWrapper nodeId={props.nodeProps.id} selected={props.nodeProps.selected} width={384}>
<Flex
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={DRAG_HANDLE_CLASSNAME}
position="relative"
flexDirection="column"
aspectRatio="1/1"
>
<Flex layerStyle="nodeHeader" borderTopRadius="base" alignItems="center" justifyContent="center" h={8}>
<Text fontSize="sm" fontWeight="semibold" color="base.200">
@@ -81,7 +80,7 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
)}
</Flex>
</Flex>
</NonInvocationNodeWrapper>
</NodeWrapper>
);
};

View File

@@ -3,8 +3,8 @@ import { createSelector } from '@reduxjs/toolkit';
import type { Node, NodeProps } from '@xyflow/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NonInvocationNodeTitle from 'features/nodes/components/flow/nodes/common/NonInvocationNodeTitle';
import NonInvocationNodeWrapper from 'features/nodes/components/flow/nodes/common/NonInvocationNodeWrapper';
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
import { NO_DRAG_CLASS, NO_PAN_CLASS } from 'features/nodes/types/constants';
@@ -34,7 +34,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
}
return (
<NonInvocationNodeWrapper nodeId={nodeId} selected={selected}>
<NodeWrapper nodeId={nodeId} selected={selected}>
<Flex
layerStyle="nodeHeader"
borderTopRadius="base"
@@ -44,7 +44,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
h={8}
>
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<NonInvocationNodeTitle nodeId={nodeId} title="Notes" />
<NodeTitle nodeId={nodeId} title="Notes" />
<Box minW={8} />
</Flex>
{isOpen && (
@@ -73,7 +73,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
</Flex>
</>
)}
</NonInvocationNodeWrapper>
</NodeWrapper>
);
};

View File

@@ -1,4 +1,4 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
@@ -12,8 +12,6 @@ import { zNodeStatus } from 'features/nodes/types/invocation';
import type { MouseEvent, PropsWithChildren } from 'react';
import { memo, useCallback } from 'react';
import { containerSx, inProgressSx, shadowsSx } from './shared';
type NodeWrapperProps = PropsWithChildren & {
nodeId: string;
selected: boolean;
@@ -21,6 +19,100 @@ type NodeWrapperProps = PropsWithChildren & {
isMissingTemplate?: boolean;
};
// Certain CSS transitions are disabled as a performance optimization - they can cause massive slowdowns in large
// workflows even when the animations are GPU-accelerated CSS.
const containerSx: SystemStyleObject = {
h: 'full',
position: 'relative',
borderRadius: 'base',
transitionProperty: 'none',
cursor: 'grab',
'--border-color': 'var(--invoke-colors-base-500)',
'--border-color-selected': 'var(--invoke-colors-blue-300)',
'--header-bg-color': 'var(--invoke-colors-base-900)',
'&[data-status="warning"]': {
'--border-color': 'var(--invoke-colors-warning-500)',
'--border-color-selected': 'var(--invoke-colors-warning-500)',
'--header-bg-color': 'var(--invoke-colors-warning-700)',
},
'&[data-status="error"]': {
'--border-color': 'var(--invoke-colors-error-500)',
'--border-color-selected': 'var(--invoke-colors-error-500)',
'--header-bg-color': 'var(--invoke-colors-error-700)',
},
// The action buttons are hidden by default and shown on hover
'& .node-selection-overlay': {
display: 'block',
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
transitionProperty: 'none',
pointerEvents: 'none',
shadow: '0 0 0 1px var(--border-color)',
},
'&[data-is-mouse-over-node="true"] .node-selection-overlay': {
display: 'block',
},
'&[data-is-mouse-over-form-field="true"] .node-selection-overlay': {
display: 'block',
bg: 'invokeBlueAlpha.100',
},
_hover: {
'& .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 1px var(--border-color-selected)',
},
'&[data-is-selected="true"] .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 2px var(--border-color-selected)',
},
},
'&[data-is-selected="true"] .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 2px var(--border-color-selected)',
},
'&[data-is-editor-locked="true"]': {
'& *': {
cursor: 'not-allowed',
pointerEvents: 'none',
},
},
};
const shadowsSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
pointerEvents: 'none',
zIndex: -1,
shadow: 'var(--invoke-shadows-xl), var(--invoke-shadows-base), var(--invoke-shadows-base)',
};
const inProgressSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'md',
pointerEvents: 'none',
transitionProperty: 'none',
opacity: 0.7,
zIndex: -1,
display: 'none',
shadow: '0 0 0 2px var(--invoke-colors-yellow-400), 0 0 20px 2px var(--invoke-colors-orange-700)',
'&[data-is-in-progress="true"]': {
display: 'block',
},
};
const NodeWrapper = (props: NodeWrapperProps) => {
const { nodeId, width, children, isMissingTemplate, selected } = props;
const ctx = useInvocationNodeContext();

View File

@@ -1,69 +0,0 @@
import { Flex, Input, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEditable } from 'common/hooks/useEditable';
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
title: string;
};
const NonInvocationNodeTitle = ({ nodeId, title }: Props) => {
const dispatch = useAppDispatch();
const selectNodeLabel = useMemo(
() =>
createSelector(selectNodes, (nodes) => {
const node = nodes.find((n) => n.id === nodeId);
return node?.data?.label ?? '';
}),
[nodeId]
);
const label = useAppSelector(selectNodeLabel);
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const onChange = useCallback(
(label: string) => {
dispatch(nodeLabelChanged({ nodeId, label }));
},
[dispatch, nodeId]
);
const editable = useEditable({
value: label || title || t('nodes.problemSettingTitle'),
defaultValue: title || t('nodes.problemSettingTitle'),
onChange,
inputRef,
});
return (
<Flex overflow="hidden" w="full" h="full" alignItems="center" justifyContent="center">
{!editable.isEditing && (
<Text
className={NO_FIT_ON_DOUBLE_CLICK_CLASS}
fontWeight="semibold"
color="base.200"
onDoubleClick={editable.startEditing}
noOfLines={1}
>
{editable.value}
</Text>
)}
{editable.isEditing && (
<Input
ref={inputRef}
{...editable.inputProps}
variant="outline"
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
/>
)}
</Flex>
);
};
export default memo(NonInvocationNodeTitle);

View File

@@ -1,80 +0,0 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
import { DRAG_HANDLE_CLASSNAME, NO_FIT_ON_DOUBLE_CLICK_CLASS, NODE_WIDTH } from 'features/nodes/types/constants';
import { zNodeStatus } from 'features/nodes/types/invocation';
import type { MouseEvent, PropsWithChildren } from 'react';
import { memo, useCallback } from 'react';
import { containerSx, inProgressSx, shadowsSx } from './shared';
type NonInvocationNodeWrapperProps = PropsWithChildren & {
nodeId: string;
selected: boolean;
width?: ChakraProps['w'];
};
const NonInvocationNodeWrapper = (props: NonInvocationNodeWrapperProps) => {
const { nodeId, width, children, selected } = props;
const mouseOverNode = useMouseOverNode(nodeId);
const zoomToNode = useZoomToNode(nodeId);
const isLocked = useIsWorkflowEditorLocked();
const executionState = useNodeExecutionState(nodeId);
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
const opacity = useAppSelector(selectNodeOpacity);
const globalMenu = useGlobalMenuClose();
const onDoubleClick = useCallback(
(e: MouseEvent) => {
if (!(e.target instanceof HTMLElement)) {
// We have to manually narrow the type here thanks to a TS quirk
return;
}
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement ||
e.target instanceof HTMLButtonElement ||
e.target instanceof HTMLAnchorElement
) {
// Don't fit the view if the user is editing a text field, select, button, or link
return;
}
if (e.target.closest(`.${NO_FIT_ON_DOUBLE_CLICK_CLASS}`) !== null) {
// This target is marked as not fitting the view on double click
return;
}
zoomToNode();
},
[zoomToNode]
);
return (
<Box
onClick={globalMenu.onCloseGlobal}
onDoubleClick={onDoubleClick}
onMouseOver={mouseOverNode.handleMouseOver}
onMouseOut={mouseOverNode.handleMouseOut}
className={DRAG_HANDLE_CLASSNAME}
sx={containerSx}
width={width || NODE_WIDTH}
opacity={opacity}
data-is-editor-locked={isLocked}
data-is-selected={selected}
>
<Box sx={shadowsSx} />
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
{children}
<Box className="node-selection-overlay" />
</Box>
);
};
export default memo(NonInvocationNodeWrapper);

View File

@@ -1,95 +0,0 @@
// Certain CSS transitions are disabled as a performance optimization - they can cause massive slowdowns in large
// workflows even when the animations are GPU-accelerated CSS.
import type { SystemStyleObject } from '@invoke-ai/ui-library';
export const containerSx: SystemStyleObject = {
h: 'full',
position: 'relative',
borderRadius: 'base',
transitionProperty: 'none',
cursor: 'grab',
'--border-color': 'var(--invoke-colors-base-500)',
'--border-color-selected': 'var(--invoke-colors-blue-300)',
'--header-bg-color': 'var(--invoke-colors-base-900)',
'&[data-status="warning"]': {
'--border-color': 'var(--invoke-colors-warning-500)',
'--border-color-selected': 'var(--invoke-colors-warning-500)',
'--header-bg-color': 'var(--invoke-colors-warning-700)',
},
'&[data-status="error"]': {
'--border-color': 'var(--invoke-colors-error-500)',
'--border-color-selected': 'var(--invoke-colors-error-500)',
'--header-bg-color': 'var(--invoke-colors-error-700)',
},
// The action buttons are hidden by default and shown on hover
'& .node-selection-overlay': {
display: 'block',
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
transitionProperty: 'none',
pointerEvents: 'none',
shadow: '0 0 0 1px var(--border-color)',
},
'&[data-is-mouse-over-node="true"] .node-selection-overlay': {
display: 'block',
},
'&[data-is-mouse-over-form-field="true"] .node-selection-overlay': {
display: 'block',
bg: 'invokeBlueAlpha.100',
},
_hover: {
'& .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 1px var(--border-color-selected)',
},
'&[data-is-selected="true"] .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 2px var(--border-color-selected)',
},
},
'&[data-is-selected="true"] .node-selection-overlay': {
display: 'block',
shadow: '0 0 0 2px var(--border-color-selected)',
},
'&[data-is-editor-locked="true"]': {
'& *': {
cursor: 'not-allowed',
pointerEvents: 'none',
},
},
};
export const shadowsSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'base',
pointerEvents: 'none',
zIndex: -1,
shadow: 'var(--invoke-shadows-xl), var(--invoke-shadows-base), var(--invoke-shadows-base)',
};
export const inProgressSx: SystemStyleObject = {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'md',
pointerEvents: 'none',
transitionProperty: 'none',
opacity: 0.7,
zIndex: -1,
display: 'none',
shadow: '0 0 0 2px var(--invoke-colors-yellow-400), 0 0 20px 2px var(--invoke-colors-orange-700)',
'&[data-is-in-progress="true"]': {
display: 'block',
},
};

View File

@@ -1,194 +0,0 @@
import {
Button,
CompositeNumberInput,
CompositeSlider,
Divider,
Flex,
FormControl,
FormLabel,
Grid,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Select,
} from '@invoke-ai/ui-library';
import { useReactFlow } from '@xyflow/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { useAutoLayout } from 'features/nodes/hooks/useAutoLayout';
import {
layeringStrategyChanged,
layerSpacingChanged,
layoutDirectionChanged,
nodeAlignmentChanged,
nodeSpacingChanged,
selectLayeringStrategy,
selectLayerSpacing,
selectLayoutDirection,
selectNodeAlignment,
selectNodeSpacing,
zLayeringStrategy,
zLayoutDirection,
zNodeAlignment,
} from 'features/nodes/store/workflowSettingsSlice';
import { type ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMagicWandBold } from 'react-icons/pi';
const [useLayoutSettingsPopover] = buildUseBoolean(false);
export const AutoLayoutPopover = memo(() => {
const { t } = useTranslation();
const { fitView } = useReactFlow();
const autoLayout = useAutoLayout();
const dispatch = useAppDispatch();
const popover = useLayoutSettingsPopover();
const layeringStrategy = useAppSelector(selectLayeringStrategy);
const nodeSpacing = useAppSelector(selectNodeSpacing);
const layerSpacing = useAppSelector(selectLayerSpacing);
const layoutDirection = useAppSelector(selectLayoutDirection);
const nodeAlignment = useAppSelector(selectNodeAlignment);
const handleLayeringStrategyChanged = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const val = zLayeringStrategy.parse(e.target.value);
dispatch(layeringStrategyChanged(val));
},
[dispatch]
);
const handleNodeSpacingSliderChange = useCallback(
(v: number) => {
dispatch(nodeSpacingChanged(v));
},
[dispatch]
);
const handleNodeSpacingInputChange = useCallback(
(v: number) => {
dispatch(nodeSpacingChanged(v));
},
[dispatch]
);
const handleLayerSpacingSliderChange = useCallback(
(v: number) => {
dispatch(layerSpacingChanged(v));
},
[dispatch]
);
const handleLayerSpacingInputChange = useCallback(
(v: number) => {
dispatch(layerSpacingChanged(v));
},
[dispatch]
);
const handleLayoutDirectionChanged = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const val = zLayoutDirection.parse(e.target.value);
dispatch(layoutDirectionChanged(val));
},
[dispatch]
);
const handleNodeAlignmentChanged = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const val = zNodeAlignment.parse(e.target.value);
dispatch(nodeAlignmentChanged(val));
},
[dispatch]
);
const handleApplyAutoLayout = useCallback(() => {
autoLayout();
fitView({ duration: 300 });
popover.setFalse();
}, [autoLayout, fitView, popover]);
return (
<Popover isOpen={popover.isTrue} onClose={popover.setFalse} placement="top">
<PopoverTrigger>
<IconButton
tooltip={t('nodes.layout.autoLayout')}
aria-label={t('nodes.layout.autoLayout')}
icon={<PiMagicWandBold />}
onClick={popover.toggle}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl>
<FormLabel>{t('nodes.layout.layoutDirection')}</FormLabel>
<Select size="sm" value={layoutDirection} onChange={handleLayoutDirectionChanged}>
<option value="LR">{t('nodes.layout.layoutDirectionRight')}</option>
<option value="TB">{t('nodes.layout.layoutDirectionDown')}</option>
</Select>
</FormControl>
<FormControl>
<FormLabel>{t('nodes.layout.layeringStrategy')}</FormLabel>
<Select size="sm" value={layeringStrategy} onChange={handleLayeringStrategyChanged}>
<option value="network-simplex">{t('nodes.layout.networkSimplex')}</option>
<option value="longest-path">{t('nodes.layout.longestPath')}</option>
</Select>
</FormControl>
<FormControl>
<FormLabel>{t('nodes.layout.alignment')}</FormLabel>
<Select size="sm" value={nodeAlignment} onChange={handleNodeAlignmentChanged}>
<option value="UL">{t('nodes.layout.alignmentUL')}</option>
<option value="DL">{t('nodes.layout.alignmentDL')}</option>
<option value="UR">{t('nodes.layout.alignmentUR')}</option>
<option value="DR">{t('nodes.layout.alignmentDR')}</option>
</Select>
</FormControl>
<Divider />
<FormControl>
<FormLabel>{t('nodes.layout.nodeSpacing')}</FormLabel>
<Grid w="full" gap={2} templateColumns="1fr auto">
<CompositeSlider min={0} max={200} value={nodeSpacing} onChange={handleNodeSpacingSliderChange} marks />
<CompositeNumberInput
value={nodeSpacing}
min={0}
max={200}
onChange={handleNodeSpacingInputChange}
w={24}
/>
</Grid>
</FormControl>
<FormControl>
<FormLabel>{t('nodes.layout.layerSpacing')}</FormLabel>
<Grid w="full" gap={2} templateColumns="1fr auto">
<CompositeSlider
min={0}
max={200}
value={layerSpacing}
onChange={handleLayerSpacingSliderChange}
marks
/>
<CompositeNumberInput
value={layerSpacing}
min={0}
max={200}
onChange={handleLayerSpacingInputChange}
w={24}
/>
</Grid>
</FormControl>
<Divider />
<Button w="full" onClick={handleApplyAutoLayout}>
{t('common.apply')}
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
AutoLayoutPopover.displayName = 'AutoLayoutPopover';

View File

@@ -14,8 +14,6 @@ import {
PiMapPinBold,
} from 'react-icons/pi';
import { AutoLayoutPopover } from './AutoLayoutPopover';
const ViewportControls = () => {
const { t } = useTranslation();
const { zoomIn, zoomOut, fitView } = useReactFlow();
@@ -58,7 +56,20 @@ const ViewportControls = () => {
onClick={handleClickedFitView}
icon={<PiFrameCornersBold />}
/>
<AutoLayoutPopover />
{/* <Tooltip
label={
shouldShowFieldTypeLegend
? t('nodes.hideLegendNodes')
: t('nodes.showLegendNodes')
}
>
<IconButton
aria-label="Toggle field type legend"
isChecked={shouldShowFieldTypeLegend}
onClick={handleClickedToggleFieldTypeLegend}
icon={<FaInfo />}
/>
</Tooltip> */}
<IconButton
tooltip={shouldShowMinimapPanel ? t('nodes.hideMinimapnodes') : t('nodes.showMinimapnodes')}
aria-label={shouldShowMinimapPanel ? t('nodes.hideMinimapnodes') : t('nodes.showMinimapnodes')}

View File

@@ -18,7 +18,6 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon
import { withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
import { ExternalLink } from 'features/gallery/components/ImageViewer/NoContentForViewer';
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
import { NodeFieldElementOverlay } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
import {
@@ -90,11 +89,7 @@ const OutputFields = memo(() => {
{t('workflows.builder.noOutputNodeSelected')}
</Text>
)}
{outputNodeId && (
<InvocationNodeContextProvider nodeId={outputNodeId}>
<OutputFieldsContent outputNodeId={outputNodeId} />
</InvocationNodeContextProvider>
)}
{outputNodeId && <OutputFieldsContent outputNodeId={outputNodeId} />}
</Flex>
);
});
@@ -132,11 +127,7 @@ const PublishableInputFields = memo(() => {
<Text fontWeight="semibold">{t('workflows.builder.publishedWorkflowInputs')}</Text>
<Divider />
{inputs.publishable.map(({ nodeId, fieldName }) => {
return (
<InvocationNodeContextProvider nodeId={nodeId} key={`${nodeId}-${fieldName}`}>
<NodeInputFieldPreview nodeId={nodeId} fieldName={fieldName} />
</InvocationNodeContextProvider>
);
return <NodeInputFieldPreview key={`${nodeId}-${fieldName}`} nodeId={nodeId} fieldName={fieldName} />;
})}
</Flex>
);
@@ -158,11 +149,7 @@ const UnpublishableInputFields = memo(() => {
</Text>
<Divider />
{inputs.unpublishable.map(({ nodeId, fieldName }) => {
return (
<InvocationNodeContextProvider nodeId={nodeId} key={`${nodeId}-${fieldName}`}>
<NodeInputFieldPreview key={`${nodeId}-${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
</InvocationNodeContextProvider>
);
return <NodeInputFieldPreview key={`${nodeId}-${fieldName}`} nodeId={nodeId} fieldName={fieldName} />;
})}
</Flex>
);

View File

@@ -1,139 +0,0 @@
import { graphlib, layout } from '@dagrejs/dagre';
import type { Edge, NodePositionChange } from '@xyflow/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { nodesChanged } from 'features/nodes/store/nodesSlice';
import { selectEdges, selectNodes } from 'features/nodes/store/selectors';
import {
selectLayeringStrategy,
selectLayerSpacing,
selectLayoutDirection,
selectNodeAlignment,
selectNodeSpacing,
} from 'features/nodes/store/workflowSettingsSlice';
import { NODE_WIDTH } from 'features/nodes/types/constants';
import type { AnyNode } from 'features/nodes/types/invocation';
import { isNotesNode } from 'features/nodes/types/invocation';
import { useCallback } from 'react';
const ESTIMATED_NOTES_NODE_HEIGHT = 200;
const DEFAULT_NODE_HEIGHT = NODE_WIDTH;
const getNodeHeight = (node: AnyNode): number => {
if (node.measured?.height) {
return node.measured.height;
}
if (isNotesNode(node)) {
return ESTIMATED_NOTES_NODE_HEIGHT;
}
return DEFAULT_NODE_HEIGHT;
};
const getNodeWidth = (node: AnyNode): number => {
if (node.measured?.width) {
return node.measured.width;
}
return NODE_WIDTH;
};
export const useAutoLayout = (): (() => void) => {
const store = useAppStore();
const nodeSpacing = useAppSelector(selectNodeSpacing);
const layerSpacing = useAppSelector(selectLayerSpacing);
const layeringStrategy = useAppSelector(selectLayeringStrategy);
const layoutDirection = useAppSelector(selectLayoutDirection);
const nodeAlignment = useAppSelector(selectNodeAlignment);
const autoLayout = useCallback(() => {
const state = store.getState();
const nodes = selectNodes(state);
const edges = selectEdges(state);
// We'll do graph layout using dagre, then convert the results to reactflow position changes
const g = new graphlib.Graph();
g.setGraph({
rankdir: layoutDirection,
nodesep: nodeSpacing,
ranksep: layerSpacing,
ranker: layeringStrategy,
align: nodeAlignment,
});
g.setDefaultEdgeLabel(() => ({}));
const selectedNodes = nodes.filter((n) => n.selected);
const isLayoutSelection = selectedNodes.length > 1 && nodes.length > selectedNodes.length;
const nodesToLayout = isLayoutSelection ? selectedNodes : nodes;
//Anchor of the selected nodes
const selectionAnchor = {
minX: Infinity,
minY: Infinity,
};
nodesToLayout.forEach((node) => {
// If we're laying out a selection, adjust the anchor to the top-left of the selection
if (isLayoutSelection) {
selectionAnchor.minX = Math.min(selectionAnchor.minX, node.position.x);
selectionAnchor.minY = Math.min(selectionAnchor.minY, node.position.y);
}
g.setNode(node.id, {
width: getNodeWidth(node),
height: getNodeHeight(node),
});
});
let edgesToLayout: Edge[] = edges;
if (isLayoutSelection) {
const nodesToLayoutIds = new Set(nodesToLayout.map((n) => n.id));
edgesToLayout = edges.filter((edge) => nodesToLayoutIds.has(edge.source) && nodesToLayoutIds.has(edge.target));
}
edgesToLayout.forEach((edge) => {
g.setEdge(edge.source, edge.target);
});
layout(g);
// anchor for the new layout
const layoutAnchor = {
minX: Infinity,
minY: Infinity,
};
let offsetX = 0;
let offsetY = 0;
if (isLayoutSelection) {
// Get the top-left position of the new layout
nodesToLayout.forEach((node) => {
const nodeInfo = g.node(node.id);
// Convert from center to top-left
const topLeftX = nodeInfo.x - nodeInfo.width / 2;
const topLeftY = nodeInfo.y - nodeInfo.height / 2;
// Use the top-left coordinates to find the bounding box
layoutAnchor.minX = Math.min(layoutAnchor.minX, topLeftX);
layoutAnchor.minY = Math.min(layoutAnchor.minY, topLeftY);
});
// Calculate the offset needed to move the new layout to the original position
offsetX = selectionAnchor.minX - layoutAnchor.minX;
offsetY = selectionAnchor.minY - layoutAnchor.minY;
}
// Create reactflow position changes for each node based on the new layout
const positionChanges: NodePositionChange[] = nodesToLayout.map((node) => {
const nodeInfo = g.node(node.id);
// Convert from center-based position to top-left-based position
const x = nodeInfo.x - nodeInfo.width / 2;
const y = nodeInfo.y - nodeInfo.height / 2;
const newPosition = {
x: isLayoutSelection ? x + offsetX : x,
y: isLayoutSelection ? y + offsetY : y,
};
return { id: node.id, type: 'position', position: newPosition };
});
store.dispatch(nodesChanged(positionChanges));
}, [layerSpacing, layeringStrategy, layoutDirection, nodeAlignment, nodeSpacing, store]);
return autoLayout;
};

View File

@@ -3,25 +3,12 @@ import { createSelector, createSlice } from '@reduxjs/toolkit';
import { SelectionMode } from '@xyflow/react';
import type { PersistConfig, RootState } from 'app/store/store';
import type { Selector } from 'react-redux';
import z from 'zod';
export const zLayeringStrategy = z.enum(['network-simplex', 'longest-path']);
type LayeringStrategy = z.infer<typeof zLayeringStrategy>;
export const zLayoutDirection = z.enum(['TB', 'LR']);
type LayoutDirection = z.infer<typeof zLayoutDirection>;
export const zNodeAlignment = z.enum(['UL', 'UR', 'DL', 'DR']);
type NodeAlignment = z.infer<typeof zNodeAlignment>;
export type WorkflowSettingsState = {
_version: 1;
shouldShowMinimapPanel: boolean;
layeringStrategy: LayeringStrategy;
nodeSpacing: number;
layerSpacing: number;
layoutDirection: LayoutDirection;
shouldValidateGraph: boolean;
shouldAnimateEdges: boolean;
nodeAlignment: NodeAlignment;
nodeOpacity: number;
shouldSnapToGrid: boolean;
shouldColorEdges: boolean;
@@ -32,11 +19,6 @@ export type WorkflowSettingsState = {
const initialState: WorkflowSettingsState = {
_version: 1,
shouldShowMinimapPanel: true,
layeringStrategy: 'network-simplex',
nodeSpacing: 30,
layerSpacing: 30,
layoutDirection: 'LR',
nodeAlignment: 'UL',
shouldValidateGraph: true,
shouldAnimateEdges: true,
shouldSnapToGrid: false,
@@ -53,18 +35,6 @@ export const workflowSettingsSlice = createSlice({
shouldShowMinimapPanelChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowMinimapPanel = action.payload;
},
layeringStrategyChanged: (state, action: PayloadAction<LayeringStrategy>) => {
state.layeringStrategy = action.payload;
},
nodeSpacingChanged: (state, action: PayloadAction<number>) => {
state.nodeSpacing = action.payload;
},
layerSpacingChanged: (state, action: PayloadAction<number>) => {
state.layerSpacing = action.payload;
},
layoutDirectionChanged: (state, action: PayloadAction<LayoutDirection>) => {
state.layoutDirection = action.payload;
},
shouldValidateGraphChanged: (state, action: PayloadAction<boolean>) => {
state.shouldValidateGraph = action.payload;
},
@@ -83,9 +53,6 @@ export const workflowSettingsSlice = createSlice({
nodeOpacityChanged: (state, action: PayloadAction<number>) => {
state.nodeOpacity = action.payload;
},
nodeAlignmentChanged: (state, action: PayloadAction<NodeAlignment>) => {
state.nodeAlignment = action.payload;
},
selectionModeChanged: (state, action: PayloadAction<boolean>) => {
state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial;
},
@@ -96,13 +63,8 @@ export const {
shouldAnimateEdgesChanged,
shouldColorEdgesChanged,
shouldShowMinimapPanelChanged,
layeringStrategyChanged,
nodeSpacingChanged,
layerSpacingChanged,
layoutDirectionChanged,
shouldShowEdgeLabelsChanged,
shouldSnapToGridChanged,
nodeAlignmentChanged,
shouldValidateGraphChanged,
nodeOpacityChanged,
selectionModeChanged,
@@ -134,9 +96,3 @@ export const selectShouldShowEdgeLabels = createWorkflowSettingsSelector((s) =>
export const selectNodeOpacity = createWorkflowSettingsSelector((s) => s.nodeOpacity);
export const selectShouldShowMinimapPanel = createWorkflowSettingsSelector((s) => s.shouldShowMinimapPanel);
export const selectShouldShouldValidateGraph = createWorkflowSettingsSelector((s) => s.shouldValidateGraph);
export const selectLayeringStrategy = createWorkflowSettingsSelector((s) => s.layeringStrategy);
export const selectNodeSpacing = createWorkflowSettingsSelector((s) => s.nodeSpacing);
export const selectLayerSpacing = createWorkflowSettingsSelector((s) => s.layerSpacing);
export const selectLayoutDirection = createWorkflowSettingsSelector((s) => s.layoutDirection);
export const selectNodeAlignment = createWorkflowSettingsSelector((s) => s.nodeAlignment);

View File

@@ -4,7 +4,6 @@ import { range } from 'es-toolkit/compat';
import type { SeedBehaviour } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import type { ModelIdentifierField } from 'features/nodes/types/common';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import type { components } from 'services/api/schema';
import type { Batch, EnqueueBatchArg, Invocation } from 'services/api/types';
import { assert } from 'tsafe';
@@ -19,7 +18,7 @@ const getExtendedPrompts = (arg: {
// Normally, the seed behaviour implicity determines the batch size. But when we use models without seeds (like
// ChatGPT 4o) in conjunction with the per-prompt seed behaviour, we lose out on that implicit batch size. To rectify
// this, we need to create a batch of the right size by repeating the prompts.
if (seedBehaviour === 'PER_PROMPT' || API_BASE_MODELS.includes(model.base)) {
if (seedBehaviour === 'PER_PROMPT' || model.base === 'chatgpt-4o' || model.base === 'flux-kontext') {
return range(iterations).flatMap(() => prompts);
}
return prompts;

View File

@@ -164,7 +164,6 @@ const removeStarred = <T,>(obj: WithStarred<T>): T => {
export const ModelPicker = typedMemo(
<T extends AnyModelConfig = AnyModelConfig>({
pickerId,
modelConfigs,
selectedModelConfig,
onChange,
@@ -178,7 +177,6 @@ export const ModelPicker = typedMemo(
noOptionsText,
initialGroupStates,
}: {
pickerId: string;
modelConfigs: T[];
selectedModelConfig: T | undefined;
onChange: (modelConfig: T) => void;
@@ -348,7 +346,6 @@ export const ModelPicker = typedMemo(
<PopoverArrow />
<PopoverBody p={0} w="full" h="full" borderWidth={1} borderColor="base.700" borderRadius="base">
<Picker<WithStarred<T>>
pickerId={pickerId}
handleRef={pickerRef}
optionsOrGroups={options}
getOptionId={getOptionId<T>}
@@ -418,15 +415,16 @@ const optionNameSx: SystemStyleObject = {
const PickerOptionComponent = typedMemo(
<T extends AnyModelConfig>({ option, ...rest }: { option: WithStarred<T> } & BoxProps) => {
const { isCompactView } = usePickerContext<WithStarred<T>>();
const { $compactView } = usePickerContext<WithStarred<T>>();
const compactView = useStore($compactView);
return (
<Flex {...rest} sx={optionSx} data-is-compact={isCompactView}>
{!isCompactView && option.cover_image && <ModelImage image_url={option.cover_image} />}
<Flex {...rest} sx={optionSx} data-is-compact={compactView}>
{!compactView && option.cover_image && <ModelImage image_url={option.cover_image} />}
<Flex flexDir="column" gap={1} flex={1}>
<Flex gap={2} alignItems="center">
{option.starred && <Icon as={PiLinkSimple} color="invokeYellow.500" boxSize={4} />}
<Text className="picker-option" sx={optionNameSx} data-is-compact={isCompactView}>
<Text className="picker-option" sx={optionNameSx} data-is-compact={compactView}>
{option.name}
</Text>
<Spacer />
@@ -455,7 +453,7 @@ const PickerOptionComponent = typedMemo(
</Text>
)}
</Flex>
{option.description && !isCompactView && (
{option.description && !compactView && (
<Text className="extra-info" color="base.200">
{option.description}
</Text>

View File

@@ -56,7 +56,6 @@ const ParamTileControlNetModel = () => {
<FormLabel m={0}>{t('upscaling.tileControl')}</FormLabel>
</InformationalPopover>
<ModelPicker
pickerId="tile-controlnet-model"
modelConfigs={filteredModelConfigs}
selectedModelConfig={tileControlNetModel ?? undefined}
onChange={_onChange}

View File

@@ -13,13 +13,14 @@ export const CancelAllExceptCurrentButton = memo((props: ButtonProps) => {
<Button
isDisabled={api.isDisabled}
isLoading={api.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
leftIcon={<PiXCircle />}
colorScheme="error"
onClick={api.openDialog}
{...props}
>
{t('queue.cancelAllExceptCurrentTooltip')}
{t('queue.clear')}
</Button>
);
});

View File

@@ -1,29 +0,0 @@
import type { ButtonProps } from '@invoke-ai/ui-library';
import { Button } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashBold } from 'react-icons/pi';
import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
export const ClearQueueButton = memo((props: ButtonProps) => {
const { t } = useTranslation();
const api = useClearQueueDialog();
return (
<Button
isDisabled={api.isDisabled}
isLoading={api.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.clearTooltip')}
leftIcon={<PiTrashBold />}
colorScheme="error"
onClick={api.openDialog}
{...props}
>
{t('queue.clear')}
</Button>
);
});
ClearQueueButton.displayName = 'ClearQueueButton';

View File

@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
export const useClearQueueDialog = () => {
const useClearQueueDialog = () => {
const dialog = useClearQueueConfirmationAlertDialog();
const clearQueue = useClearQueue();

View File

@@ -9,19 +9,15 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashBold, PiXBold, PiXCircle } from 'react-icons/pi';
import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiXBold, PiXCircle } from 'react-icons/pi';
export const QueueActionsMenuButton = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
const isClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
const clearQueue = useClearQueueDialog();
const resumeProcessor = useResumeProcessor();
const pauseProcessor = usePauseProcessor();
const openQueue = useCallback(() => {
@@ -59,17 +55,6 @@ export const QueueActionsMenuButton = memo(() => {
>
{t('queue.cancelAllExceptCurrentTooltip')}
</MenuItem>
{isClearAllEnabled && (
<MenuItem
isDestructive
icon={<PiTrashBold />}
onClick={clearQueue.openDialog}
isLoading={clearQueue.isLoading}
isDisabled={clearQueue.isDisabled}
>
{t('queue.clearTooltip')}
</MenuItem>
)}
{isResumeEnabled && (
<MenuItem
icon={<PiPlayFill />}

View File

@@ -4,7 +4,6 @@ import { memo } from 'react';
import { CancelAllExceptCurrentButton } from './CancelAllExceptCurrentButton';
import ClearModelCacheButton from './ClearModelCacheButton';
import { ClearQueueButton } from './ClearQueueButton';
import PauseProcessorButton from './PauseProcessorButton';
import PruneQueueButton from './PruneQueueButton';
import ResumeProcessorButton from './ResumeProcessorButton';
@@ -12,20 +11,19 @@ import ResumeProcessorButton from './ResumeProcessorButton';
const QueueTabQueueControls = () => {
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
const isClearQueueEnabled = useFeatureStatus('cancelAndClearAll');
return (
<Flex flexDir="column" layerStyle="first" borderRadius="base" p={2} gap={2}>
<Flex gap={2}>
{(isPauseEnabled || isResumeEnabled) && (
<ButtonGroup orientation="vertical" size="sm">
<ButtonGroup w={28} orientation="vertical" size="sm">
{isResumeEnabled && <ResumeProcessorButton />}
{isPauseEnabled && <PauseProcessorButton />}
</ButtonGroup>
)}
<ButtonGroup orientation="vertical" size="sm">
<ButtonGroup w={28} orientation="vertical" size="sm">
<PruneQueueButton />
{isClearQueueEnabled ? <ClearQueueButton /> : <CancelAllExceptCurrentButton />}
<CancelAllExceptCurrentButton />
</ButtonGroup>
</Flex>
<ClearModelCacheButton />

View File

@@ -43,13 +43,7 @@ export const MainModelPicker = memo(() => {
</Flex>
</InformationalPopover>
)}
<ModelPicker
pickerId="main-model"
modelConfigs={modelConfigs}
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
/>
<ModelPicker modelConfigs={modelConfigs} selectedModelConfig={selectedModelConfig} onChange={onChange} grouped />
<UseDefaultSettingsButton />
</Flex>
);

View File

@@ -112,7 +112,6 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('canvas', 'resetSelected', ['shift+c']);
addHotkey('canvas', 'transformSelected', ['shift+t']);
addHotkey('canvas', 'filterSelected', ['shift+f']);
addHotkey('canvas', 'invertMask', ['shift+v']);
addHotkey('canvas', 'undo', ['mod+z']);
addHotkey('canvas', 'redo', ['mod+shift+z', 'mod+y']);
addHotkey('canvas', 'nextEntity', ['alt+]']);
@@ -124,7 +123,6 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('canvas', 'applySegmentAnything', ['enter']);
addHotkey('canvas', 'cancelSegmentAnything', ['esc']);
addHotkey('canvas', 'toggleNonRasterLayers', ['shift+h']);
addHotkey('canvas', 'fitBboxToMasks', ['shift+b']);
// Workflows
addHotkey('workflows', 'addNode', ['shift+a', 'space']);
@@ -160,7 +158,6 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('gallery', 'galleryNavDownAlt', ['alt+down']);
addHotkey('gallery', 'galleryNavLeftAlt', ['alt+left']);
addHotkey('gallery', 'deleteSelection', ['delete', 'backspace']);
addHotkey('gallery', 'starImage', ['.']);
return data;
}, [isMacOS, isModelManagerEnabled, t]);

View File

@@ -40,13 +40,7 @@ export const InitialStateMainModelPicker = memo(() => {
</InformationalPopover>
)}
</FormLabel>
<ModelPicker
pickerId="initial-state-main-model"
modelConfigs={modelConfigs}
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
/>
<ModelPicker modelConfigs={modelConfigs} selectedModelConfig={selectedModelConfig} onChange={onChange} grouped />
</FormControl>
);
});

View File

@@ -1,5 +1,5 @@
import type { DockviewApi, GridviewApi } from 'dockview';
import { DockviewApi as MockedDockviewApi, DockviewPanel, GridviewPanel } from 'dockview';
import { DockviewPanel, GridviewPanel } from 'dockview';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { NavigationAppApi } from './navigation-api';
@@ -12,7 +12,6 @@ import {
RIGHT_PANEL_MIN_SIZE_PX,
SETTINGS_PANEL_ID,
SWITCH_TABS_FAKE_DELAY_MS,
VIEWER_PANEL_ID,
WORKSPACE_PANEL_ID,
} from './shared';
@@ -49,7 +48,7 @@ vi.mock('dockview', async () => {
}
}
// Mock DockviewPanel class for instanceof checks
// Mock GridviewPanel class for instanceof checks
class MockDockviewPanel {
api = {
setActive: vi.fn(),
@@ -59,21 +58,10 @@ vi.mock('dockview', async () => {
};
}
// Mock DockviewApi class for instanceof checks
class MockDockviewApi {
panels = [];
activePanel = null;
toJSON = vi.fn();
fromJSON = vi.fn();
onDidLayoutChange = vi.fn();
onDidActivePanelChange = vi.fn();
}
return {
...actual,
GridviewPanel: MockGridviewPanel,
DockviewPanel: MockDockviewPanel,
DockviewApi: MockDockviewApi,
};
});
@@ -1117,393 +1105,4 @@ describe('AppNavigationApi', () => {
expect(initialize).not.toHaveBeenCalled();
});
});
describe('toggleViewerPanel', () => {
beforeEach(() => {
navigationApi.connectToApp(mockAppApi);
});
it('should switch to viewer panel when not currently on viewer', async () => {
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current panel to something other than viewer
navigationApi._currentActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(true);
expect(mockViewerPanel.api.setActive).toHaveBeenCalledOnce();
});
it('should switch to previous panel when on viewer and previous panel exists', async () => {
const mockPreviousPanel = createMockDockPanel();
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockPreviousPanel);
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current panel to viewer and previous to settings
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(true);
expect(mockPreviousPanel.api.setActive).toHaveBeenCalledOnce();
expect(mockViewerPanel.api.setActive).not.toHaveBeenCalled();
});
it('should switch to launchpad when on viewer and no valid previous panel', async () => {
const mockLaunchpadPanel = createMockDockPanel();
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockLaunchpadPanel);
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current panel to viewer and no previous panel
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', null);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(true);
expect(mockLaunchpadPanel.api.setActive).toHaveBeenCalledOnce();
expect(mockViewerPanel.api.setActive).not.toHaveBeenCalled();
});
it('should switch to launchpad when on viewer and previous panel is also viewer', async () => {
const mockLaunchpadPanel = createMockDockPanel();
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockLaunchpadPanel);
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current panel to viewer and previous panel was also viewer
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(true);
expect(mockLaunchpadPanel.api.setActive).toHaveBeenCalledOnce();
expect(mockViewerPanel.api.setActive).not.toHaveBeenCalled();
});
it('should return false when no active tab', async () => {
mockGetAppTab.mockReturnValue(null);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(false);
});
it('should return false when viewer panel is not registered', async () => {
mockGetAppTab.mockReturnValue('generate');
navigationApi._currentActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
// Don't register viewer panel
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(false);
});
it('should return false when previous panel is not registered', async () => {
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current to viewer and previous to unregistered panel
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', 'unregistered-panel');
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(false);
});
it('should return false when launchpad panel is not registered as fallback', async () => {
const mockViewerPanel = createMockDockPanel();
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
mockGetAppTab.mockReturnValue('generate');
// Set current to viewer and no previous panel, but don't register launchpad
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', null);
const result = await navigationApi.toggleViewerPanel();
expect(result).toBe(false);
});
it('should work across different tabs independently', async () => {
const mockViewerPanel1 = createMockDockPanel();
const mockViewerPanel2 = createMockDockPanel();
const mockSettingsPanel1 = createMockDockPanel();
const mockSettingsPanel2 = createMockDockPanel();
const mockLaunchpadPanel = createMockDockPanel();
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel1);
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockSettingsPanel1);
navigationApi._registerPanel('canvas', VIEWER_PANEL_ID, mockViewerPanel2);
navigationApi._registerPanel('canvas', SETTINGS_PANEL_ID, mockSettingsPanel2);
navigationApi._registerPanel('canvas', LAUNCHPAD_PANEL_ID, mockLaunchpadPanel);
// Set up different states for different tabs
navigationApi._currentActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
navigationApi._currentActiveDockviewPanel.set('canvas', VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('canvas', SETTINGS_PANEL_ID);
// Test generate tab (should switch to viewer)
mockGetAppTab.mockReturnValue('generate');
const result1 = await navigationApi.toggleViewerPanel();
expect(result1).toBe(true);
expect(mockViewerPanel1.api.setActive).toHaveBeenCalledOnce();
// Test canvas tab (should switch to previous panel - settings panel in canvas)
mockGetAppTab.mockReturnValue('canvas');
const result2 = await navigationApi.toggleViewerPanel();
expect(result2).toBe(true);
expect(mockSettingsPanel2.api.setActive).toHaveBeenCalledOnce();
});
it('should handle sequence of viewer toggles correctly', async () => {
const mockViewerPanel = createMockDockPanel();
const mockSettingsPanel = createMockDockPanel();
const mockLaunchpadPanel = createMockDockPanel();
navigationApi._registerPanel('generate', VIEWER_PANEL_ID, mockViewerPanel);
navigationApi._registerPanel('generate', SETTINGS_PANEL_ID, mockSettingsPanel);
navigationApi._registerPanel('generate', LAUNCHPAD_PANEL_ID, mockLaunchpadPanel);
mockGetAppTab.mockReturnValue('generate');
// Start on settings panel
navigationApi._currentActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set('generate', null);
// First toggle: settings -> viewer
const result1 = await navigationApi.toggleViewerPanel();
expect(result1).toBe(true);
expect(mockViewerPanel.api.setActive).toHaveBeenCalledOnce();
// Simulate panel change tracking (normally done by dockview listener)
navigationApi._prevActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
navigationApi._currentActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
// Second toggle: viewer -> settings (previous panel)
const result2 = await navigationApi.toggleViewerPanel();
expect(result2).toBe(true);
expect(mockSettingsPanel.api.setActive).toHaveBeenCalledOnce();
// Simulate panel change tracking again
navigationApi._prevActiveDockviewPanel.set('generate', VIEWER_PANEL_ID);
navigationApi._currentActiveDockviewPanel.set('generate', SETTINGS_PANEL_ID);
// Third toggle: settings -> viewer again
const result3 = await navigationApi.toggleViewerPanel();
expect(result3).toBe(true);
expect(mockViewerPanel.api.setActive).toHaveBeenCalledTimes(2);
});
});
describe('Disposable Cleanup', () => {
beforeEach(() => {
navigationApi.connectToApp(mockAppApi);
});
it('should add disposable functions for a tab', () => {
const dispose1 = vi.fn();
const dispose2 = vi.fn();
navigationApi._addDisposeForTab('generate', dispose1);
navigationApi._addDisposeForTab('generate', dispose2);
// Check that disposables are stored
const disposables = navigationApi._disposablesForTab.get('generate');
expect(disposables).toBeDefined();
expect(disposables?.size).toBe(2);
expect(disposables?.has(dispose1)).toBe(true);
expect(disposables?.has(dispose2)).toBe(true);
});
it('should handle multiple tabs independently', () => {
const dispose1 = vi.fn();
const dispose2 = vi.fn();
const dispose3 = vi.fn();
navigationApi._addDisposeForTab('generate', dispose1);
navigationApi._addDisposeForTab('generate', dispose2);
navigationApi._addDisposeForTab('canvas', dispose3);
const generateDisposables = navigationApi._disposablesForTab.get('generate');
const canvasDisposables = navigationApi._disposablesForTab.get('canvas');
expect(generateDisposables?.size).toBe(2);
expect(canvasDisposables?.size).toBe(1);
expect(generateDisposables?.has(dispose1)).toBe(true);
expect(generateDisposables?.has(dispose2)).toBe(true);
expect(canvasDisposables?.has(dispose3)).toBe(true);
});
it('should call all dispose functions when unregistering a tab', () => {
const dispose1 = vi.fn();
const dispose2 = vi.fn();
const dispose3 = vi.fn();
// Add disposables for generate tab
navigationApi._addDisposeForTab('generate', dispose1);
navigationApi._addDisposeForTab('generate', dispose2);
// Add disposable for canvas tab (should not be called)
navigationApi._addDisposeForTab('canvas', dispose3);
// Unregister generate tab
navigationApi.unregisterTab('generate');
// Check that generate tab disposables were called
expect(dispose1).toHaveBeenCalledOnce();
expect(dispose2).toHaveBeenCalledOnce();
// Check that canvas tab disposable was not called
expect(dispose3).not.toHaveBeenCalled();
// Check that generate tab disposables are cleared
expect(navigationApi._disposablesForTab.has('generate')).toBe(false);
// Check that canvas tab disposables remain
expect(navigationApi._disposablesForTab.has('canvas')).toBe(true);
});
it('should handle unregistering tab with no disposables gracefully', () => {
// Should not throw when unregistering tab with no disposables
expect(() => navigationApi.unregisterTab('generate')).not.toThrow();
});
it('should handle duplicate dispose functions', () => {
const dispose1 = vi.fn();
// Add the same dispose function twice
navigationApi._addDisposeForTab('generate', dispose1);
navigationApi._addDisposeForTab('generate', dispose1);
const disposables = navigationApi._disposablesForTab.get('generate');
// Set should contain only one instance (sets don't allow duplicates)
expect(disposables?.size).toBe(1);
navigationApi.unregisterTab('generate');
// Should be called only once despite being added twice
expect(dispose1).toHaveBeenCalledOnce();
});
it('should automatically add dispose functions during container registration with DockviewApi', () => {
const tab = 'generate';
const viewId = 'myView';
mockGetStorage.mockReturnValue(undefined);
const initialize = vi.fn();
const panel = { id: 'p1' };
const mockDispose = vi.fn();
// Create a mock that will pass the instanceof DockviewApi check
const mockApi = Object.create(MockedDockviewApi.prototype);
Object.assign(mockApi, {
panels: [panel],
activePanel: { id: 'p1' },
toJSON: vi.fn(() => ({ foo: 'bar' })),
onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })),
onDidActivePanelChange: vi.fn(() => ({ dispose: mockDispose })),
});
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
// Check that dispose function was added to disposables
const disposables = navigationApi._disposablesForTab.get(tab);
expect(disposables).toBeDefined();
expect(disposables?.size).toBe(1);
// Unregister tab and check dispose was called
navigationApi.unregisterTab(tab);
expect(mockDispose).toHaveBeenCalledOnce();
});
it('should not add dispose functions for GridviewApi during container registration', () => {
const tab = 'generate';
const viewId = 'myView';
mockGetStorage.mockReturnValue(undefined);
const initialize = vi.fn();
const panel = { id: 'p1' };
// Mock GridviewApi (not DockviewApi)
const mockApi = {
panels: [panel],
toJSON: vi.fn(() => ({ foo: 'bar' })),
onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })),
} as unknown as GridviewApi;
navigationApi.registerContainer(tab, viewId, mockApi, initialize);
// Check that no dispose function was added for GridviewApi
const disposables = navigationApi._disposablesForTab.get(tab);
expect(disposables).toBeUndefined();
});
it('should handle dispose function errors gracefully', () => {
const goodDispose = vi.fn();
const errorDispose = vi.fn(() => {
throw new Error('Dispose error');
});
const anotherGoodDispose = vi.fn();
navigationApi._addDisposeForTab('generate', goodDispose);
navigationApi._addDisposeForTab('generate', errorDispose);
navigationApi._addDisposeForTab('generate', anotherGoodDispose);
// Should not throw even if one dispose function throws
expect(() => navigationApi.unregisterTab('generate')).not.toThrow();
// All dispose functions should have been called
expect(goodDispose).toHaveBeenCalledOnce();
expect(errorDispose).toHaveBeenCalledOnce();
expect(anotherGoodDispose).toHaveBeenCalledOnce();
});
it('should clear panel tracking state when unregistering tab', () => {
const tab = 'generate';
// Set up some panel tracking state
navigationApi._currentActiveDockviewPanel.set(tab, VIEWER_PANEL_ID);
navigationApi._prevActiveDockviewPanel.set(tab, SETTINGS_PANEL_ID);
// Add some disposables
const dispose1 = vi.fn();
const dispose2 = vi.fn();
navigationApi._addDisposeForTab(tab, dispose1);
navigationApi._addDisposeForTab(tab, dispose2);
// Verify state exists before unregistering
expect(navigationApi._currentActiveDockviewPanel.has(tab)).toBe(true);
expect(navigationApi._prevActiveDockviewPanel.has(tab)).toBe(true);
expect(navigationApi._disposablesForTab.has(tab)).toBe(true);
// Unregister tab
navigationApi.unregisterTab(tab);
// Verify all state is cleared
expect(navigationApi._currentActiveDockviewPanel.has(tab)).toBe(false);
expect(navigationApi._prevActiveDockviewPanel.has(tab)).toBe(false);
expect(navigationApi._disposablesForTab.has(tab)).toBe(false);
// Verify dispose functions were called
expect(dispose1).toHaveBeenCalledOnce();
expect(dispose2).toHaveBeenCalledOnce();
});
});
});

View File

@@ -1,21 +1,19 @@
import { logger } from 'app/logging/logger';
import { createDeferredPromise, type Deferred } from 'common/util/createDeferredPromise';
import { parseify } from 'common/util/serialize';
import type { GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview';
import { DockviewApi, GridviewPanel } from 'dockview';
import type { DockviewApi, GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview';
import { GridviewPanel } from 'dockview';
import { debounce } from 'es-toolkit';
import type { Serializable, TabName } from 'features/ui/store/uiTypes';
import type { Atom } from 'nanostores';
import { atom } from 'nanostores';
import {
LAUNCHPAD_PANEL_ID,
LEFT_PANEL_ID,
LEFT_PANEL_MIN_SIZE_PX,
RIGHT_PANEL_ID,
RIGHT_PANEL_MIN_SIZE_PX,
SWITCH_TABS_FAKE_DELAY_MS,
VIEWER_PANEL_ID,
} from './shared';
const log = logger('system');
@@ -71,37 +69,6 @@ export class NavigationApi {
private _$isLoading = atom(false);
$isLoading: Atom<boolean> = this._$isLoading;
/**
* Track the _previous_ active dockview panel for each tab.
*/
_prevActiveDockviewPanel: Map<TabName, string | null> = new Map();
/**
* Track the _current_ active dockview panel for each tab.
*/
_currentActiveDockviewPanel: Map<TabName, string | null> = new Map();
/**
* Map of disposables for each tab.
* This is used to clean up resources when a tab is unregistered.
*/
_disposablesForTab: Map<TabName, Set<() => void>> = new Map();
/**
* Convenience method to add a dispose function for a specific tab.
*/
/**
* Convenience method to add a dispose function for a specific tab.
*/
_addDisposeForTab = (tab: TabName, disposeFn: () => void): void => {
let disposables = this._disposablesForTab.get(tab);
if (!disposables) {
disposables = new Set<() => void>();
this._disposablesForTab.set(tab, disposables);
}
disposables.add(disposeFn);
};
/**
* Separator used to create unique keys for panels. Typo protection.
*/
@@ -242,18 +209,6 @@ export class NavigationApi {
this._registerPanel(tab, panel.id, panel);
}
// Set up tracking for active tab for this panel - needed for viewer toggle functionality
if (api instanceof DockviewApi) {
this._currentActiveDockviewPanel.set(tab, api.activePanel?.id ?? null);
this._prevActiveDockviewPanel.set(tab, null);
const { dispose } = api.onDidActivePanelChange((panel) => {
const previousPanelId = this._currentActiveDockviewPanel.get(tab);
this._prevActiveDockviewPanel.set(tab, previousPanelId ?? null);
this._currentActiveDockviewPanel.set(tab, panel?.id ?? null);
});
this._addDisposeForTab(tab, dispose);
}
api.onDidLayoutChange(
debounce(() => {
this._app?.storage.set(key, api.toJSON());
@@ -590,42 +545,6 @@ export class NavigationApi {
return true;
};
/**
* Toggle between the viewer panel and the previously focused dockview panel in the current tab.
* If currently on viewer and a previous panel exists, switch to the previous panel.
* If not on viewer, switch to viewer.
* If no previous panel exists, defaults to launchpad panel.
* Only operates on dockview panels (panels with tabs), not gridview panels.
*
* @returns Promise that resolves to true if successful, false otherwise
*/
toggleViewerPanel = (): Promise<boolean> => {
const activeTab = this._app?.activeTab.get() ?? null;
if (!activeTab) {
log.warn('No active tab found for viewer toggle');
return Promise.resolve(false);
}
const prevActiveDockviewPanel = this._prevActiveDockviewPanel.get(activeTab);
const currentActiveDockviewPanel = this._currentActiveDockviewPanel.get(activeTab);
let targetPanel;
if (currentActiveDockviewPanel !== VIEWER_PANEL_ID) {
targetPanel = VIEWER_PANEL_ID;
} else if (prevActiveDockviewPanel && prevActiveDockviewPanel !== VIEWER_PANEL_ID) {
targetPanel = prevActiveDockviewPanel;
} else {
targetPanel = LAUNCHPAD_PANEL_ID;
}
if (this.getRegisteredPanels(activeTab).includes(targetPanel)) {
return this.focusPanel(activeTab, targetPanel);
}
return Promise.resolve(false);
};
/**
* Check if a panel is registered.
* @param tab - The tab the panel belongs to
@@ -674,18 +593,6 @@ export class NavigationApi {
this.waiters.delete(key);
}
// Clear previous panel tracking for this tab
this._prevActiveDockviewPanel.delete(tab);
this._currentActiveDockviewPanel.delete(tab);
this._disposablesForTab.get(tab)?.forEach((disposeFn) => {
try {
disposeFn();
} catch (error) {
log.error({ error: parseify(error) }, `Error disposing resource for tab ${tab}`);
}
});
this._disposablesForTab.delete(tab);
log.trace(`Unregistered all panels for tab ${tab}`);
};
}

View File

@@ -4,4 +4,3 @@ import { selectUiSlice } from 'features/ui/store/uiSlice';
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectPickerCompactViewStates = createSelector(selectUiSlice, (ui) => ui.pickerCompactViewStates);

View File

@@ -65,9 +65,6 @@ export const uiSlice = createSlice({
shouldShowNotificationChanged: (state, action: PayloadAction<UIState['shouldShowNotificationV2']>) => {
state.shouldShowNotificationV2 = action.payload;
},
pickerCompactViewStateChanged: (state, action: PayloadAction<{ pickerId: string; isCompact: boolean }>) => {
state.pickerCompactViewStates[action.payload.pickerId] = action.payload.isCompact;
},
},
});
@@ -80,7 +77,6 @@ export const {
shouldShowNotificationChanged,
textAreaSizesStateChanged,
dockviewStorageKeyChanged,
pickerCompactViewStateChanged,
} = uiSlice.actions;
export const selectUiSlice = (state: RootState) => state.ui;

View File

@@ -23,7 +23,6 @@ const zUIState = z.object({
textAreaSizes: z.record(z.string(), zPartialDimensions).default({}),
panels: z.record(z.string(), zSerializable).default({}),
shouldShowNotificationV2: z.boolean().default(true),
pickerCompactViewStates: z.record(z.string(), z.boolean()).default(() => ({})),
});
const INITIAL_STATE = zUIState.parse({});
export type UIState = z.infer<typeof zUIState>;

View File

@@ -1 +1 @@
__version__ = "6.2.0"
__version__ = "6.1.0rc2"