mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'feat/model_manager/search-by-format' of github.com:invoke-ai/InvokeAI into feat/model_manager/search-by-format
This commit is contained in:
commit
72dca55e44
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@ -42,6 +42,21 @@ Please provide steps on how to test changes, any hardware or
|
|||||||
software specifications as well as any other pertinent information.
|
software specifications as well as any other pertinent information.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Merge Plan
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A merge plan describes how this PR should be handled after it is approved.
|
||||||
|
|
||||||
|
Example merge plans:
|
||||||
|
- "This PR can be merged when approved"
|
||||||
|
- "This must be squash-merged when approved"
|
||||||
|
- "DO NOT MERGE - I will rebase and tidy commits before merging"
|
||||||
|
- "#dev-chat on discord needs to be advised of this change when it is merged"
|
||||||
|
|
||||||
|
A merge plan is particularly important for large PRs or PRs that touch the
|
||||||
|
database in any way.
|
||||||
|
-->
|
||||||
|
|
||||||
## Added/updated tests?
|
## Added/updated tests?
|
||||||
|
|
||||||
- [ ] Yes
|
- [ ] Yes
|
||||||
|
@ -293,6 +293,19 @@ manager, please follow these steps:
|
|||||||
|
|
||||||
## Developer Install
|
## Developer Install
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
InvokeAI uses a SQLite database. By running on `main`, you accept responsibility for your database. This
|
||||||
|
means making regular backups (especially before pulling) and/or fixing it yourself in the event that a
|
||||||
|
PR introduces a schema change.
|
||||||
|
|
||||||
|
If you don't need persistent backend storage, you can use an ephemeral in-memory database by setting
|
||||||
|
`use_memory_db: true` under `Path:` in your `invokeai.yaml` file.
|
||||||
|
|
||||||
|
If this is untenable, you should run the application via the official installer or a manual install of the
|
||||||
|
python package from pypi. These releases will not break your database.
|
||||||
|
|
||||||
|
|
||||||
If you have an interest in how InvokeAI works, or you would like to
|
If you have an interest in how InvokeAI works, or you would like to
|
||||||
add features or bugfixes, you are encouraged to install the source
|
add features or bugfixes, you are encouraged to install the source
|
||||||
code for InvokeAI. For this to work, you will need to install the
|
code for InvokeAI. For this to work, you will need to install the
|
||||||
@ -388,3 +401,5 @@ environment variable INVOKEAI_ROOT to point to the installation directory.
|
|||||||
|
|
||||||
Note that if you run into problems with the Conda installation, the InvokeAI
|
Note that if you run into problems with the Conda installation, the InvokeAI
|
||||||
staff will **not** be able to help you out. Caveat Emptor!
|
staff will **not** be able to help you out. Caveat Emptor!
|
||||||
|
|
||||||
|
[dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
|
10
docs/javascripts/init_kapa_widget.js
Normal file
10
docs/javascripts/init_kapa_widget.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = "https://widget.kapa.ai/kapa-widget.bundle.js";
|
||||||
|
script.setAttribute("data-website-id", "b5973bb1-476b-451e-8cf4-98de86745a10");
|
||||||
|
script.setAttribute("data-project-name", "Invoke.AI");
|
||||||
|
script.setAttribute("data-project-color", "#11213C");
|
||||||
|
script.setAttribute("data-project-logo", "https://avatars.githubusercontent.com/u/113954515?s=280&v=4");
|
||||||
|
script.async = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
@ -13,7 +13,15 @@ from invokeai.app.shared.fields import FieldDescriptions
|
|||||||
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
||||||
from invokeai.backend.image_util.safety_checker import SafetyChecker
|
from invokeai.backend.image_util.safety_checker import SafetyChecker
|
||||||
|
|
||||||
from .baseinvocation import BaseInvocation, Input, InputField, InvocationContext, WithMetadata, invocation
|
from .baseinvocation import (
|
||||||
|
BaseInvocation,
|
||||||
|
Classification,
|
||||||
|
Input,
|
||||||
|
InputField,
|
||||||
|
InvocationContext,
|
||||||
|
WithMetadata,
|
||||||
|
invocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0")
|
@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0")
|
||||||
@ -421,6 +429,64 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"unsharp_mask",
|
||||||
|
title="Unsharp Mask",
|
||||||
|
tags=["image", "unsharp_mask"],
|
||||||
|
category="image",
|
||||||
|
version="1.2.0",
|
||||||
|
classification=Classification.Beta,
|
||||||
|
)
|
||||||
|
class UnsharpMaskInvocation(BaseInvocation, WithMetadata):
|
||||||
|
"""Applies an unsharp mask filter to an image"""
|
||||||
|
|
||||||
|
image: ImageField = InputField(description="The image to use")
|
||||||
|
radius: float = InputField(gt=0, description="Unsharp mask radius", default=2)
|
||||||
|
strength: float = InputField(ge=0, description="Unsharp mask strength", default=50)
|
||||||
|
|
||||||
|
def pil_from_array(self, arr):
|
||||||
|
return Image.fromarray((arr * 255).astype("uint8"))
|
||||||
|
|
||||||
|
def array_from_pil(self, img):
|
||||||
|
return numpy.array(img) / 255
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
|
image = context.services.images.get_pil_image(self.image.image_name)
|
||||||
|
mode = image.mode
|
||||||
|
|
||||||
|
alpha_channel = image.getchannel("A") if mode == "RGBA" else None
|
||||||
|
image = image.convert("RGB")
|
||||||
|
image_blurred = self.array_from_pil(image.filter(ImageFilter.GaussianBlur(radius=self.radius)))
|
||||||
|
|
||||||
|
image = self.array_from_pil(image)
|
||||||
|
image += (image - image_blurred) * (self.strength / 100.0)
|
||||||
|
image = numpy.clip(image, 0, 1)
|
||||||
|
image = self.pil_from_array(image)
|
||||||
|
|
||||||
|
image = image.convert(mode)
|
||||||
|
|
||||||
|
# Make the image RGBA if we had a source alpha channel
|
||||||
|
if alpha_channel is not None:
|
||||||
|
image.putalpha(alpha_channel)
|
||||||
|
|
||||||
|
image_dto = context.services.images.create(
|
||||||
|
image=image,
|
||||||
|
image_origin=ResourceOrigin.INTERNAL,
|
||||||
|
image_category=ImageCategory.GENERAL,
|
||||||
|
node_id=self.id,
|
||||||
|
session_id=context.graph_execution_state_id,
|
||||||
|
is_intermediate=self.is_intermediate,
|
||||||
|
metadata=self.metadata,
|
||||||
|
workflow=context.workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ImageOutput(
|
||||||
|
image=ImageField(image_name=image_dto.image_name),
|
||||||
|
width=image.width,
|
||||||
|
height=image.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
PIL_RESAMPLING_MODES = Literal[
|
PIL_RESAMPLING_MODES = Literal[
|
||||||
"nearest",
|
"nearest",
|
||||||
"box",
|
"box",
|
||||||
|
@ -38,7 +38,14 @@ class CalculateImageTilesOutput(BaseInvocationOutput):
|
|||||||
tiles: list[Tile] = OutputField(description="The tiles coordinates that cover a particular image shape.")
|
tiles: list[Tile] = OutputField(description="The tiles coordinates that cover a particular image shape.")
|
||||||
|
|
||||||
|
|
||||||
@invocation("calculate_image_tiles", title="Calculate Image Tiles", tags=["tiles"], category="tiles", version="1.0.0")
|
@invocation(
|
||||||
|
"calculate_image_tiles",
|
||||||
|
title="Calculate Image Tiles",
|
||||||
|
tags=["tiles"],
|
||||||
|
category="tiles",
|
||||||
|
version="1.0.0",
|
||||||
|
classification=Classification.Beta,
|
||||||
|
)
|
||||||
class CalculateImageTilesInvocation(BaseInvocation):
|
class CalculateImageTilesInvocation(BaseInvocation):
|
||||||
"""Calculate the coordinates and overlaps of tiles that cover a target image shape."""
|
"""Calculate the coordinates and overlaps of tiles that cover a target image shape."""
|
||||||
|
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
|
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
|
||||||
|
from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
|
||||||
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
|
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
|
||||||
|
from invokeai.app.services.workflow_records.workflow_records_common import (
|
||||||
|
UnsafeWorkflowWithVersionValidator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration2Callback:
|
class Migration2Callback:
|
||||||
@ -134,7 +139,7 @@ class Migration2Callback:
|
|||||||
This migrate callback checks each image for the presence of an embedded workflow, then updates its entry
|
This migrate callback checks each image for the presence of an embedded workflow, then updates its entry
|
||||||
in the database accordingly.
|
in the database accordingly.
|
||||||
"""
|
"""
|
||||||
# Get the total number of images and chunk it into pages
|
# Get all image names
|
||||||
cursor.execute("SELECT image_name FROM images")
|
cursor.execute("SELECT image_name FROM images")
|
||||||
image_names: list[str] = [image[0] for image in cursor.fetchall()]
|
image_names: list[str] = [image[0] for image in cursor.fetchall()]
|
||||||
total_image_names = len(image_names)
|
total_image_names = len(image_names)
|
||||||
@ -149,8 +154,17 @@ class Migration2Callback:
|
|||||||
pbar = tqdm(image_names)
|
pbar = tqdm(image_names)
|
||||||
for idx, image_name in enumerate(pbar):
|
for idx, image_name in enumerate(pbar):
|
||||||
pbar.set_description(f"Checking image {idx + 1}/{total_image_names} for workflow")
|
pbar.set_description(f"Checking image {idx + 1}/{total_image_names} for workflow")
|
||||||
|
try:
|
||||||
pil_image = self._image_files.get(image_name)
|
pil_image = self._image_files.get(image_name)
|
||||||
|
except ImageFileNotFoundException:
|
||||||
|
self._logger.warning(f"Image {image_name} not found, skipping")
|
||||||
|
continue
|
||||||
if "invokeai_workflow" in pil_image.info:
|
if "invokeai_workflow" in pil_image.info:
|
||||||
|
try:
|
||||||
|
UnsafeWorkflowWithVersionValidator.validate_json(pil_image.info.get("invokeai_workflow", ""))
|
||||||
|
except ValidationError:
|
||||||
|
self._logger.warning(f"Image {image_name} has invalid embedded workflow, skipping")
|
||||||
|
continue
|
||||||
to_migrate.append((True, image_name))
|
to_migrate.append((True, image_name))
|
||||||
|
|
||||||
self._logger.info(f"Adding {len(to_migrate)} embedded workflows to database")
|
self._logger.info(f"Adding {len(to_migrate)} embedded workflows to database")
|
||||||
|
@ -65,12 +65,24 @@ class WorkflowWithoutID(BaseModel):
|
|||||||
nodes: list[dict[str, JsonValue]] = Field(description="The nodes of the workflow.")
|
nodes: list[dict[str, JsonValue]] = Field(description="The nodes of the workflow.")
|
||||||
edges: list[dict[str, JsonValue]] = Field(description="The edges of the workflow.")
|
edges: list[dict[str, JsonValue]] = Field(description="The edges of the workflow.")
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
WorkflowWithoutIDValidator = TypeAdapter(WorkflowWithoutID)
|
WorkflowWithoutIDValidator = TypeAdapter(WorkflowWithoutID)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeWorkflowWithVersion(BaseModel):
|
||||||
|
"""
|
||||||
|
This utility model only requires a workflow to have a valid version string.
|
||||||
|
It is used to validate a workflow version without having to validate the entire workflow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
meta: WorkflowMeta = Field(description="The meta of the workflow.")
|
||||||
|
|
||||||
|
|
||||||
|
UnsafeWorkflowWithVersionValidator = TypeAdapter(UnsafeWorkflowWithVersion)
|
||||||
|
|
||||||
|
|
||||||
class Workflow(WorkflowWithoutID):
|
class Workflow(WorkflowWithoutID):
|
||||||
id: str = Field(description="The id of the workflow.")
|
id: str = Field(description="The id of the workflow.")
|
||||||
|
|
||||||
|
@ -950,9 +950,9 @@
|
|||||||
"problemSettingTitle": "Problem Setting Title",
|
"problemSettingTitle": "Problem Setting Title",
|
||||||
"reloadNodeTemplates": "Reload Node Templates",
|
"reloadNodeTemplates": "Reload Node Templates",
|
||||||
"removeLinearView": "Remove from Linear View",
|
"removeLinearView": "Remove from Linear View",
|
||||||
"resetWorkflow": "Reset Workflow Editor",
|
"newWorkflow": "New Workflow",
|
||||||
"resetWorkflowDesc": "Are you sure you want to reset the Workflow Editor?",
|
"newWorkflowDesc": "Create a new workflow?",
|
||||||
"resetWorkflowDesc2": "Resetting the Workflow Editor will clear all nodes, edges and workflow details. Saved workflows will not be affected.",
|
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
|
||||||
"scheduler": "Scheduler",
|
"scheduler": "Scheduler",
|
||||||
"schedulerDescription": "TODO",
|
"schedulerDescription": "TODO",
|
||||||
"sDXLMainModelField": "SDXL Model",
|
"sDXLMainModelField": "SDXL Model",
|
||||||
@ -1634,10 +1634,10 @@
|
|||||||
"userWorkflows": "My Workflows",
|
"userWorkflows": "My Workflows",
|
||||||
"defaultWorkflows": "Default Workflows",
|
"defaultWorkflows": "Default Workflows",
|
||||||
"openWorkflow": "Open Workflow",
|
"openWorkflow": "Open Workflow",
|
||||||
"uploadWorkflow": "Upload Workflow",
|
"uploadWorkflow": "Load from File",
|
||||||
"deleteWorkflow": "Delete Workflow",
|
"deleteWorkflow": "Delete Workflow",
|
||||||
"unnamedWorkflow": "Unnamed Workflow",
|
"unnamedWorkflow": "Unnamed Workflow",
|
||||||
"downloadWorkflow": "Download Workflow",
|
"downloadWorkflow": "Save to File",
|
||||||
"saveWorkflow": "Save Workflow",
|
"saveWorkflow": "Save Workflow",
|
||||||
"saveWorkflowAs": "Save Workflow As",
|
"saveWorkflowAs": "Save Workflow As",
|
||||||
"savingWorkflow": "Saving Workflow...",
|
"savingWorkflow": "Saving Workflow...",
|
||||||
@ -1652,7 +1652,7 @@
|
|||||||
"searchWorkflows": "Search Workflows",
|
"searchWorkflows": "Search Workflows",
|
||||||
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
|
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
|
||||||
"workflowName": "Workflow Name",
|
"workflowName": "Workflow Name",
|
||||||
"workflowEditorReset": "Workflow Editor Reset",
|
"newWorkflowCreated": "New Workflow Created",
|
||||||
"workflowEditorMenu": "Workflow Editor Menu",
|
"workflowEditorMenu": "Workflow Editor Menu",
|
||||||
"workflowIsOpen": "Workflow is Open"
|
"workflowIsOpen": "Workflow is Open"
|
||||||
},
|
},
|
||||||
|
@ -104,7 +104,16 @@
|
|||||||
"copyError": "$t(gallery.copy) Errore",
|
"copyError": "$t(gallery.copy) Errore",
|
||||||
"input": "Ingresso",
|
"input": "Ingresso",
|
||||||
"notInstalled": "Non $t(common.installed)",
|
"notInstalled": "Non $t(common.installed)",
|
||||||
"unknownError": "Errore sconosciuto"
|
"unknownError": "Errore sconosciuto",
|
||||||
|
"updated": "Aggiornato",
|
||||||
|
"save": "Salva",
|
||||||
|
"created": "Creato",
|
||||||
|
"prevPage": "Pagina precedente",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"orderBy": "Ordinato per",
|
||||||
|
"nextPage": "Pagina successiva",
|
||||||
|
"saveAs": "Salva come",
|
||||||
|
"unsaved": "Non salvato"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"generations": "Generazioni",
|
"generations": "Generazioni",
|
||||||
@ -763,7 +772,10 @@
|
|||||||
"setIPAdapterImage": "Imposta come immagine per l'Adattatore IP",
|
"setIPAdapterImage": "Imposta come immagine per l'Adattatore IP",
|
||||||
"problemSavingMaskDesc": "Impossibile salvare la maschera",
|
"problemSavingMaskDesc": "Impossibile salvare la maschera",
|
||||||
"setAsCanvasInitialImage": "Imposta come immagine iniziale della tela",
|
"setAsCanvasInitialImage": "Imposta come immagine iniziale della tela",
|
||||||
"invalidUpload": "Caricamento non valido"
|
"invalidUpload": "Caricamento non valido",
|
||||||
|
"problemDeletingWorkflow": "Problema durante l'eliminazione del flusso di lavoro",
|
||||||
|
"workflowDeleted": "Flusso di lavoro eliminato",
|
||||||
|
"problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"feature": {
|
"feature": {
|
||||||
@ -886,11 +898,11 @@
|
|||||||
"zoomInNodes": "Ingrandire",
|
"zoomInNodes": "Ingrandire",
|
||||||
"fitViewportNodes": "Adatta vista",
|
"fitViewportNodes": "Adatta vista",
|
||||||
"showGraphNodes": "Mostra sovrapposizione grafico",
|
"showGraphNodes": "Mostra sovrapposizione grafico",
|
||||||
"resetWorkflowDesc2": "Reimpostare il flusso di lavoro cancellerà tutti i nodi, i bordi e i dettagli del flusso di lavoro.",
|
"resetWorkflowDesc2": "Il ripristino dell'editor del flusso di lavoro cancellerà tutti i nodi, le connessioni e i dettagli del flusso di lavoro. I flussi di lavoro salvati non saranno interessati.",
|
||||||
"reloadNodeTemplates": "Ricarica i modelli di nodo",
|
"reloadNodeTemplates": "Ricarica i modelli di nodo",
|
||||||
"loadWorkflow": "Importa flusso di lavoro JSON",
|
"loadWorkflow": "Importa flusso di lavoro JSON",
|
||||||
"resetWorkflow": "Reimposta flusso di lavoro",
|
"resetWorkflow": "Reimposta l'editor del flusso di lavoro",
|
||||||
"resetWorkflowDesc": "Sei sicuro di voler reimpostare questo flusso di lavoro?",
|
"resetWorkflowDesc": "Sei sicuro di voler reimpostare l'editor del flusso di lavoro?",
|
||||||
"downloadWorkflow": "Esporta flusso di lavoro JSON",
|
"downloadWorkflow": "Esporta flusso di lavoro JSON",
|
||||||
"scheduler": "Campionatore",
|
"scheduler": "Campionatore",
|
||||||
"addNode": "Aggiungi nodo",
|
"addNode": "Aggiungi nodo",
|
||||||
@ -1080,25 +1092,27 @@
|
|||||||
"collectionOrScalarFieldType": "{{name}} Raccolta|Scalare",
|
"collectionOrScalarFieldType": "{{name}} Raccolta|Scalare",
|
||||||
"nodeVersion": "Versione Nodo",
|
"nodeVersion": "Versione Nodo",
|
||||||
"inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})",
|
"inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})",
|
||||||
"unsupportedArrayItemType": "tipo di elemento dell'array non supportato \"{{type}}\"",
|
"unsupportedArrayItemType": "Tipo di elemento dell'array non supportato \"{{type}}\"",
|
||||||
"targetNodeFieldDoesNotExist": "Connessione non valida: il campo di destinazione/input {{node}}.{{field}} non esiste",
|
"targetNodeFieldDoesNotExist": "Connessione non valida: il campo di destinazione/input {{node}}.{{field}} non esiste",
|
||||||
"unsupportedMismatchedUnion": "tipo CollectionOrScalar non corrispondente con tipi di base {{firstType}} e {{secondType}}",
|
"unsupportedMismatchedUnion": "tipo CollectionOrScalar non corrispondente con tipi di base {{firstType}} e {{secondType}}",
|
||||||
"allNodesUpdated": "Tutti i nodi sono aggiornati",
|
"allNodesUpdated": "Tutti i nodi sono aggiornati",
|
||||||
"sourceNodeDoesNotExist": "Connessione non valida: il nodo di origine/output {{node}} non esiste",
|
"sourceNodeDoesNotExist": "Connessione non valida: il nodo di origine/output {{node}} non esiste",
|
||||||
"unableToExtractEnumOptions": "impossibile estrarre le opzioni enum",
|
"unableToExtractEnumOptions": "Impossibile estrarre le opzioni enum",
|
||||||
"unableToParseFieldType": "impossibile analizzare il tipo di campo",
|
"unableToParseFieldType": "Impossibile analizzare il tipo di campo",
|
||||||
"unrecognizedWorkflowVersion": "Versione dello schema del flusso di lavoro non riconosciuta {{version}}",
|
"unrecognizedWorkflowVersion": "Versione dello schema del flusso di lavoro non riconosciuta {{version}}",
|
||||||
"outputFieldTypeParseError": "Impossibile analizzare il tipo di campo di output {{node}}.{{field}} ({{message}})",
|
"outputFieldTypeParseError": "Impossibile analizzare il tipo di campo di output {{node}}.{{field}} ({{message}})",
|
||||||
"sourceNodeFieldDoesNotExist": "Connessione non valida: il campo di origine/output {{node}}.{{field}} non esiste",
|
"sourceNodeFieldDoesNotExist": "Connessione non valida: il campo di origine/output {{node}}.{{field}} non esiste",
|
||||||
"unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro",
|
"unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro",
|
||||||
"nodePack": "Pacchetto di nodi",
|
"nodePack": "Pacchetto di nodi",
|
||||||
"unableToExtractSchemaNameFromRef": "impossibile estrarre il nome dello schema dal riferimento",
|
"unableToExtractSchemaNameFromRef": "Impossibile estrarre il nome dello schema dal riferimento",
|
||||||
"unknownOutput": "Output sconosciuto: {{name}}",
|
"unknownOutput": "Output sconosciuto: {{name}}",
|
||||||
"unknownNodeType": "Tipo di nodo sconosciuto",
|
"unknownNodeType": "Tipo di nodo sconosciuto",
|
||||||
"targetNodeDoesNotExist": "Connessione non valida: il nodo di destinazione/input {{node}} non esiste",
|
"targetNodeDoesNotExist": "Connessione non valida: il nodo di destinazione/input {{node}} non esiste",
|
||||||
"unknownFieldType": "$t(nodes.unknownField) tipo: {{type}}",
|
"unknownFieldType": "$t(nodes.unknownField) tipo: {{type}}",
|
||||||
"deletedInvalidEdge": "Eliminata connessione non valida {{source}} -> {{target}}",
|
"deletedInvalidEdge": "Eliminata connessione non valida {{source}} -> {{target}}",
|
||||||
"unknownInput": "Input sconosciuto: {{name}}"
|
"unknownInput": "Input sconosciuto: {{name}}",
|
||||||
|
"prototypeDesc": "Questa invocazione è un prototipo. Potrebbe subire modifiche sostanziali durante gli aggiornamenti dell'app e potrebbe essere rimossa in qualsiasi momento.",
|
||||||
|
"betaDesc": "Questa invocazione è in versione beta. Fino a quando non sarà stabile, potrebbe subire modifiche importanti durante gli aggiornamenti dell'app. Abbiamo intenzione di supportare questa invocazione a lungo termine."
|
||||||
},
|
},
|
||||||
"boards": {
|
"boards": {
|
||||||
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
||||||
@ -1594,5 +1608,34 @@
|
|||||||
"hrf": "Correzione Alta Risoluzione",
|
"hrf": "Correzione Alta Risoluzione",
|
||||||
"hrfStrength": "Forza della Correzione Alta Risoluzione",
|
"hrfStrength": "Forza della Correzione Alta Risoluzione",
|
||||||
"strengthTooltip": "Valori più bassi comportano meno dettagli, il che può ridurre potenziali artefatti."
|
"strengthTooltip": "Valori più bassi comportano meno dettagli, il che può ridurre potenziali artefatti."
|
||||||
|
},
|
||||||
|
"workflows": {
|
||||||
|
"saveWorkflowAs": "Salva flusso di lavoro come",
|
||||||
|
"workflowEditorMenu": "Menu dell'editor del flusso di lavoro",
|
||||||
|
"noSystemWorkflows": "Nessun flusso di lavoro del sistema",
|
||||||
|
"workflowName": "Nome del flusso di lavoro",
|
||||||
|
"noUserWorkflows": "Nessun flusso di lavoro utente",
|
||||||
|
"defaultWorkflows": "Flussi di lavoro predefiniti",
|
||||||
|
"saveWorkflow": "Salva flusso di lavoro",
|
||||||
|
"openWorkflow": "Apri flusso di lavoro",
|
||||||
|
"clearWorkflowSearchFilter": "Cancella il filtro di ricerca del flusso di lavoro",
|
||||||
|
"workflowEditorReset": "Reimpostazione dell'editor del flusso di lavoro",
|
||||||
|
"workflowLibrary": "Libreria",
|
||||||
|
"noRecentWorkflows": "Nessun flusso di lavoro recente",
|
||||||
|
"workflowSaved": "Flusso di lavoro salvato",
|
||||||
|
"workflowIsOpen": "Il flusso di lavoro è aperto",
|
||||||
|
"unnamedWorkflow": "Flusso di lavoro senza nome",
|
||||||
|
"savingWorkflow": "Salvataggio del flusso di lavoro...",
|
||||||
|
"problemLoading": "Problema durante il caricamento dei flussi di lavoro",
|
||||||
|
"loading": "Caricamento dei flussi di lavoro",
|
||||||
|
"searchWorkflows": "Cerca flussi di lavoro",
|
||||||
|
"problemSavingWorkflow": "Problema durante il salvataggio del flusso di lavoro",
|
||||||
|
"deleteWorkflow": "Elimina flusso di lavoro",
|
||||||
|
"workflows": "Flussi di lavoro",
|
||||||
|
"noDescription": "Nessuna descrizione",
|
||||||
|
"userWorkflows": "I miei flussi di lavoro"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"storeNotInitialized": "Il negozio non è inizializzato"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -110,7 +110,17 @@
|
|||||||
"copyError": "$t(gallery.copy) 错误",
|
"copyError": "$t(gallery.copy) 错误",
|
||||||
"input": "输入",
|
"input": "输入",
|
||||||
"notInstalled": "非 $t(common.installed)",
|
"notInstalled": "非 $t(common.installed)",
|
||||||
"delete": "删除"
|
"delete": "删除",
|
||||||
|
"updated": "已上传",
|
||||||
|
"save": "保存",
|
||||||
|
"created": "已创建",
|
||||||
|
"prevPage": "上一页",
|
||||||
|
"unknownError": "未知错误",
|
||||||
|
"direction": "指向",
|
||||||
|
"orderBy": "排序方式:",
|
||||||
|
"nextPage": "下一页",
|
||||||
|
"saveAs": "保存为",
|
||||||
|
"unsaved": "未保存"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"generations": "生成的图像",
|
"generations": "生成的图像",
|
||||||
@ -146,7 +156,11 @@
|
|||||||
"image": "图像",
|
"image": "图像",
|
||||||
"drop": "弃用",
|
"drop": "弃用",
|
||||||
"dropOrUpload": "$t(gallery.drop) 或上传",
|
"dropOrUpload": "$t(gallery.drop) 或上传",
|
||||||
"dropToUpload": "$t(gallery.drop) 以上传"
|
"dropToUpload": "$t(gallery.drop) 以上传",
|
||||||
|
"problemDeletingImagesDesc": "有一张或多张图像无法被删除",
|
||||||
|
"problemDeletingImages": "删除图像时出现问题",
|
||||||
|
"unstarImage": "取消收藏图像",
|
||||||
|
"starImage": "收藏图像"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"keyboardShortcuts": "键盘快捷键",
|
"keyboardShortcuts": "键盘快捷键",
|
||||||
@ -724,7 +738,7 @@
|
|||||||
"nodesUnrecognizedTypes": "无法加载。节点图有无法识别的节点类型",
|
"nodesUnrecognizedTypes": "无法加载。节点图有无法识别的节点类型",
|
||||||
"nodesNotValidJSON": "无效的 JSON",
|
"nodesNotValidJSON": "无效的 JSON",
|
||||||
"nodesNotValidGraph": "无效的 InvokeAi 节点图",
|
"nodesNotValidGraph": "无效的 InvokeAi 节点图",
|
||||||
"nodesLoadedFailed": "节点图加载失败",
|
"nodesLoadedFailed": "节点加载失败",
|
||||||
"modelAddedSimple": "已添加模型",
|
"modelAddedSimple": "已添加模型",
|
||||||
"modelAdded": "已添加模型: {{modelName}}",
|
"modelAdded": "已添加模型: {{modelName}}",
|
||||||
"imageSavingFailed": "图像保存失败",
|
"imageSavingFailed": "图像保存失败",
|
||||||
@ -760,7 +774,10 @@
|
|||||||
"problemImportingMask": "导入遮罩时出现问题",
|
"problemImportingMask": "导入遮罩时出现问题",
|
||||||
"baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型",
|
"baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型",
|
||||||
"setAsCanvasInitialImage": "设为画布初始图像",
|
"setAsCanvasInitialImage": "设为画布初始图像",
|
||||||
"invalidUpload": "无效的上传"
|
"invalidUpload": "无效的上传",
|
||||||
|
"problemDeletingWorkflow": "删除工作流时出现问题",
|
||||||
|
"workflowDeleted": "已删除工作流",
|
||||||
|
"problemRetrievingWorkflow": "检索工作流时发生问题"
|
||||||
},
|
},
|
||||||
"unifiedCanvas": {
|
"unifiedCanvas": {
|
||||||
"layer": "图层",
|
"layer": "图层",
|
||||||
@ -875,11 +892,11 @@
|
|||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"zoomInNodes": "放大",
|
"zoomInNodes": "放大",
|
||||||
"resetWorkflowDesc": "是否确定要清空节点图?",
|
"resetWorkflowDesc": "是否确定要重置工作流编辑器?",
|
||||||
"resetWorkflow": "清空节点图",
|
"resetWorkflow": "重置工作流编辑器",
|
||||||
"loadWorkflow": "读取节点图",
|
"loadWorkflow": "加载工作流",
|
||||||
"zoomOutNodes": "缩小",
|
"zoomOutNodes": "缩小",
|
||||||
"resetWorkflowDesc2": "重置节点图将清除所有节点、边际和节点图详情.",
|
"resetWorkflowDesc2": "重置工作流编辑器将清除所有节点、边际和节点图详情。不影响已保存的工作流。",
|
||||||
"reloadNodeTemplates": "重载节点模板",
|
"reloadNodeTemplates": "重载节点模板",
|
||||||
"hideGraphNodes": "隐藏节点图信息",
|
"hideGraphNodes": "隐藏节点图信息",
|
||||||
"fitViewportNodes": "自适应视图",
|
"fitViewportNodes": "自适应视图",
|
||||||
@ -888,7 +905,7 @@
|
|||||||
"showLegendNodes": "显示字段类型图例",
|
"showLegendNodes": "显示字段类型图例",
|
||||||
"hideLegendNodes": "隐藏字段类型图例",
|
"hideLegendNodes": "隐藏字段类型图例",
|
||||||
"showGraphNodes": "显示节点图信息",
|
"showGraphNodes": "显示节点图信息",
|
||||||
"downloadWorkflow": "下载节点图 JSON",
|
"downloadWorkflow": "下载工作流 JSON",
|
||||||
"workflowDescription": "简述",
|
"workflowDescription": "简述",
|
||||||
"versionUnknown": " 未知版本",
|
"versionUnknown": " 未知版本",
|
||||||
"noNodeSelected": "无选中的节点",
|
"noNodeSelected": "无选中的节点",
|
||||||
@ -1103,7 +1120,9 @@
|
|||||||
"collectionOrScalarFieldType": "{{name}} 合集 | 标量",
|
"collectionOrScalarFieldType": "{{name}} 合集 | 标量",
|
||||||
"nodeVersion": "节点版本",
|
"nodeVersion": "节点版本",
|
||||||
"deletedInvalidEdge": "已删除无效的边缘 {{source}} -> {{target}}",
|
"deletedInvalidEdge": "已删除无效的边缘 {{source}} -> {{target}}",
|
||||||
"unknownInput": "未知输入:{{name}}"
|
"unknownInput": "未知输入:{{name}}",
|
||||||
|
"prototypeDesc": "此调用是一个原型 (prototype)。它可能会在本项目更新期间发生破坏性更改,并且随时可能被删除。",
|
||||||
|
"betaDesc": "此调用尚处于测试阶段。在稳定之前,它可能会在项目更新期间发生破坏性更改。本项目计划长期支持这种调用。"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"resize": "直接缩放",
|
"resize": "直接缩放",
|
||||||
@ -1607,5 +1626,36 @@
|
|||||||
"hrf": "高分辨率修复",
|
"hrf": "高分辨率修复",
|
||||||
"hrfStrength": "高分辨率修复强度",
|
"hrfStrength": "高分辨率修复强度",
|
||||||
"strengthTooltip": "值越低细节越少,但可以减少部分潜在的伪影。"
|
"strengthTooltip": "值越低细节越少,但可以减少部分潜在的伪影。"
|
||||||
|
},
|
||||||
|
"workflows": {
|
||||||
|
"saveWorkflowAs": "保存工作流为",
|
||||||
|
"workflowEditorMenu": "工作流编辑器菜单",
|
||||||
|
"noSystemWorkflows": "无系统工作流",
|
||||||
|
"workflowName": "工作流名称",
|
||||||
|
"noUserWorkflows": "无用户工作流",
|
||||||
|
"defaultWorkflows": "默认工作流",
|
||||||
|
"saveWorkflow": "保存工作流",
|
||||||
|
"openWorkflow": "打开工作流",
|
||||||
|
"clearWorkflowSearchFilter": "清除工作流检索过滤器",
|
||||||
|
"workflowEditorReset": "工作流编辑器重置",
|
||||||
|
"workflowLibrary": "工作流库",
|
||||||
|
"downloadWorkflow": "下载工作流",
|
||||||
|
"noRecentWorkflows": "无最近工作流",
|
||||||
|
"workflowSaved": "已保存工作流",
|
||||||
|
"workflowIsOpen": "工作流已打开",
|
||||||
|
"unnamedWorkflow": "未命名的工作流",
|
||||||
|
"savingWorkflow": "保存工作流中...",
|
||||||
|
"problemLoading": "加载工作流时出现问题",
|
||||||
|
"loading": "加载工作流中",
|
||||||
|
"searchWorkflows": "检索工作流",
|
||||||
|
"problemSavingWorkflow": "保存工作流时出现问题",
|
||||||
|
"deleteWorkflow": "删除工作流",
|
||||||
|
"workflows": "工作流",
|
||||||
|
"noDescription": "无描述",
|
||||||
|
"uploadWorkflow": "上传工作流",
|
||||||
|
"userWorkflows": "我的工作流"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"storeNotInitialized": "商店尚未初始化"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,10 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
|||||||
const nodePack = invocationTemplate
|
const nodePack = invocationTemplate
|
||||||
? invocationTemplate.nodePack
|
? invocationTemplate.nodePack
|
||||||
: t('common.unknown');
|
: t('common.unknown');
|
||||||
|
|
||||||
(node.data as unknown as InvocationNodeData).nodePack = nodePack;
|
(node.data as unknown as InvocationNodeData).nodePack = nodePack;
|
||||||
|
// Fallback to 1.0.0 if not specified - this matches the behavior of the backend
|
||||||
|
node.data.version ||= '1.0.0';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Bump version
|
// Bump version
|
||||||
|
@ -11,44 +11,48 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { makeToast } from 'features/system/util/makeToast';
|
import { makeToast } from 'features/system/util/makeToast';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaTrash } from 'react-icons/fa';
|
import { FaCircleNodes } from 'react-icons/fa6';
|
||||||
|
|
||||||
const ResetWorkflowEditorMenuItem = () => {
|
const NewWorkflowMenuItem = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const isTouched = useAppSelector((state) => state.workflow.isTouched);
|
||||||
|
|
||||||
const handleConfirmClear = useCallback(() => {
|
const handleNewWorkflow = useCallback(() => {
|
||||||
dispatch(nodeEditorReset());
|
dispatch(nodeEditorReset());
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast(
|
addToast(
|
||||||
makeToast({
|
makeToast({
|
||||||
title: t('workflows.workflowEditorReset'),
|
title: t('workflows.newWorkflowCreated'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [dispatch, t, onClose]);
|
}, [dispatch, onClose, t]);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (!isTouched) {
|
||||||
|
handleNewWorkflow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpen();
|
||||||
|
}, [handleNewWorkflow, isTouched, onOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem as="button" icon={<FaCircleNodes />} onClick={onClick}>
|
||||||
as="button"
|
{t('nodes.newWorkflow')}
|
||||||
icon={<FaTrash />}
|
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
|
||||||
onClick={onOpen}
|
|
||||||
>
|
|
||||||
{t('nodes.resetWorkflow')}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
@ -61,13 +65,13 @@ const ResetWorkflowEditorMenuItem = () => {
|
|||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
{t('nodes.resetWorkflow')}
|
{t('nodes.newWorkflow')}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogBody py={4}>
|
<AlertDialogBody py={4}>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
<Text>{t('nodes.resetWorkflowDesc')}</Text>
|
<Text>{t('nodes.newWorkflowDesc')}</Text>
|
||||||
<Text variant="subtext">{t('nodes.resetWorkflowDesc2')}</Text>
|
<Text variant="subtext">{t('nodes.newWorkflowDesc2')}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</AlertDialogBody>
|
</AlertDialogBody>
|
||||||
|
|
||||||
@ -75,7 +79,7 @@ const ResetWorkflowEditorMenuItem = () => {
|
|||||||
<Button ref={cancelRef} onClick={onClose}>
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorScheme="error" ml={3} onClick={handleConfirmClear}>
|
<Button colorScheme="error" ml={3} onClick={handleNewWorkflow}>
|
||||||
{t('common.accept')}
|
{t('common.accept')}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@ -85,4 +89,4 @@ const ResetWorkflowEditorMenuItem = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ResetWorkflowEditorMenuItem);
|
export default memo(NewWorkflowMenuItem);
|
@ -9,7 +9,7 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
|
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
||||||
import ResetWorkflowEditorMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem';
|
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
|
||||||
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
||||||
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
|
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
|
||||||
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
|
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
|
||||||
@ -39,7 +39,7 @@ const WorkflowLibraryMenu = () => {
|
|||||||
{isWorkflowLibraryEnabled && <SaveWorkflowAsMenuItem />}
|
{isWorkflowLibraryEnabled && <SaveWorkflowAsMenuItem />}
|
||||||
<DownloadWorkflowMenuItem />
|
<DownloadWorkflowMenuItem />
|
||||||
<UploadWorkflowMenuItem />
|
<UploadWorkflowMenuItem />
|
||||||
<ResetWorkflowEditorMenuItem />
|
<NewWorkflowMenuItem />
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<SettingsMenuItem />
|
<SettingsMenuItem />
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
@ -101,6 +101,8 @@ plugins:
|
|||||||
extra_javascript:
|
extra_javascript:
|
||||||
- https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js
|
- https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js
|
||||||
- javascripts/tablesort.js
|
- javascripts/tablesort.js
|
||||||
|
- https://widget.kapa.ai/kapa-widget.bundle.js
|
||||||
|
- javascript/init_kapa_widget.js
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
analytics:
|
analytics:
|
||||||
|
Loading…
Reference in New Issue
Block a user