mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-21 13:01:27 -03:00
feat(recipes): redesign import modal with URL-first input and unified drop zone
This commit is contained in:
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "Importieren",
|
||||
"title": "Ein Rezept aus Bild oder URL importieren",
|
||||
"urlLocalPath": "URL / Lokaler Pfad",
|
||||
"uploadImage": "Bild hochladen",
|
||||
"urlSectionDescription": "Geben Sie eine Civitai-Bild-URL oder einen lokalen Dateipfad ein, um es als Rezept zu importieren.",
|
||||
"dropZoneLabel": "Bild hochladen",
|
||||
"dropZoneHint": "Bild hierher ziehen, aus der Zwischenablage einfügen oder klicken zum Durchsuchen",
|
||||
"orDivider": "oder Bild per Drag & Drop / Einfügen hinzufügen",
|
||||
"imageUrlOrPath": "Bild-URL oder Dateipfad:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... oder C:/pfad/zu/bild.png",
|
||||
"fetchImage": "Bild abrufen",
|
||||
"uploadSectionDescription": "Laden Sie ein Bild mit LoRA-Metadaten hoch, um es als Rezept zu importieren.",
|
||||
"selectImage": "Bild auswählen",
|
||||
"recipeName": "Rezeptname",
|
||||
"recipeNamePlaceholder": "Rezeptname eingeben",
|
||||
"tagsOptional": "Tags (optional)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "Bitte wählen Sie eine Bilddatei aus",
|
||||
"enterUrlOrPath": "Bitte geben Sie eine URL oder einen Dateipfad ein",
|
||||
"invalidUrl": "Bitte geben Sie eine gültige URL ein",
|
||||
"invalidInputFormat": "Bitte geben Sie eine Bild-URL oder einen lokalen Bilddateipfad ein",
|
||||
"selectLoraRoot": "Bitte wählen Sie ein LoRA-Stammverzeichnis aus"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "Import",
|
||||
"title": "Import a recipe from image or URL",
|
||||
"urlLocalPath": "URL / Local Path",
|
||||
"uploadImage": "Upload Image",
|
||||
"urlSectionDescription": "Input a Civitai image URL from civitai.com or civitai.red, or a local file path, to import as a recipe.",
|
||||
"dropZoneLabel": "Upload image",
|
||||
"dropZoneHint": "Drag & drop an image here, paste from clipboard, or click to browse",
|
||||
"orDivider": "or drag & drop / paste an image",
|
||||
"imageUrlOrPath": "Image URL or File Path:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... or https://civitai.red/images/... or C:/path/to/image.png",
|
||||
"fetchImage": "Fetch Image",
|
||||
"uploadSectionDescription": "Upload an image with LoRA metadata to import as a recipe.",
|
||||
"selectImage": "Select Image",
|
||||
"recipeName": "Recipe Name",
|
||||
"recipeNamePlaceholder": "Enter recipe name",
|
||||
"tagsOptional": "Tags (optional)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "Please select an image file",
|
||||
"enterUrlOrPath": "Please enter a URL or file path",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"invalidInputFormat": "Please enter an image URL or a local image file path",
|
||||
"selectLoraRoot": "Please select a LoRA root directory"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "Importar",
|
||||
"title": "Importar una receta desde imagen o URL",
|
||||
"urlLocalPath": "URL / Ruta local",
|
||||
"uploadImage": "Subir imagen",
|
||||
"urlSectionDescription": "Introduce una URL de imagen de Civitai o ruta de archivo local para importar como receta.",
|
||||
"dropZoneLabel": "Subir imagen",
|
||||
"dropZoneHint": "Arrastra y suelta una imagen aquí, pégala desde el portapapeles o haz clic para examinar",
|
||||
"orDivider": "o arrastra y suelta / pega una imagen",
|
||||
"imageUrlOrPath": "URL de imagen o ruta de archivo:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... o C:/ruta/a/imagen.png",
|
||||
"fetchImage": "Obtener imagen",
|
||||
"uploadSectionDescription": "Sube una imagen con metadatos de LoRA para importar como receta.",
|
||||
"selectImage": "Seleccionar imagen",
|
||||
"recipeName": "Nombre de receta",
|
||||
"recipeNamePlaceholder": "Introduce nombre de receta",
|
||||
"tagsOptional": "Etiquetas (opcional)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "Por favor selecciona un archivo de imagen",
|
||||
"enterUrlOrPath": "Por favor introduce una URL o ruta de archivo",
|
||||
"invalidUrl": "Introduce una URL válida",
|
||||
"invalidInputFormat": "Introduce la URL de una imagen o una ruta de archivo local",
|
||||
"selectLoraRoot": "Por favor selecciona un directorio raíz de LoRA"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "Importer",
|
||||
"title": "Importer une recipe depuis une image ou une URL",
|
||||
"urlLocalPath": "URL / Chemin local",
|
||||
"uploadImage": "Téléverser une image",
|
||||
"urlSectionDescription": "Saisissez une URL d'image Civitai ou un chemin de fichier local pour l'importer comme recipe.",
|
||||
"dropZoneLabel": "Téléverser une image",
|
||||
"dropZoneHint": "Glissez-déposez une image ici, collez-la depuis le presse-papiers ou cliquez pour parcourir",
|
||||
"orDivider": "ou glissez-déposez / collez une image",
|
||||
"imageUrlOrPath": "URL d'image ou chemin de fichier :",
|
||||
"urlPlaceholder": "https://civitai.com/images/... ou C:/chemin/vers/image.png",
|
||||
"fetchImage": "Récupérer l'image",
|
||||
"uploadSectionDescription": "Téléversez une image avec des métadonnées LoRA pour l'importer comme recipe.",
|
||||
"selectImage": "Sélectionner une image",
|
||||
"recipeName": "Nom de la recipe",
|
||||
"recipeNamePlaceholder": "Entrez le nom de la recipe",
|
||||
"tagsOptional": "Tags (optionnel)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "Veuillez sélectionner un fichier image",
|
||||
"enterUrlOrPath": "Veuillez entrer une URL ou un chemin de fichier",
|
||||
"invalidUrl": "Veuillez saisir une URL valide",
|
||||
"invalidInputFormat": "Veuillez saisir l'URL d'une image ou un chemin de fichier local",
|
||||
"selectLoraRoot": "Veuillez sélectionner un répertoire racine LoRA"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "ייבא",
|
||||
"title": "ייבא מתכון מתמונה או כתובת URL",
|
||||
"urlLocalPath": "URL / נתיב מקומי",
|
||||
"uploadImage": "העלה תמונה",
|
||||
"urlSectionDescription": "הזן כתובת URL של תמונה מ-Civitai או נתיב קובץ מקומי לייבוא כמתכון.",
|
||||
"dropZoneLabel": "העלאת תמונה",
|
||||
"dropZoneHint": "גררו ושחררו תמונה כאן, הדביקו מהלוח או לחצו לעיון",
|
||||
"orDivider": "או גררו ושחררו / הדביקו תמונה",
|
||||
"imageUrlOrPath": "URL של תמונה או נתיב קובץ:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... או C:/path/to/image.png",
|
||||
"fetchImage": "אחזר תמונה",
|
||||
"uploadSectionDescription": "העלה תמונה עם מטא-דאטה של LoRA לייבוא כמתכון.",
|
||||
"selectImage": "בחר תמונה",
|
||||
"recipeName": "שם המתכון",
|
||||
"recipeNamePlaceholder": "הזן שם מתכון",
|
||||
"tagsOptional": "תגיות (אופציונלי)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "אנא בחר קובץ תמונה",
|
||||
"enterUrlOrPath": "אנא הזן URL או נתיב קובץ",
|
||||
"invalidUrl": "נא להזין כתובת URL תקינה",
|
||||
"invalidInputFormat": "נא להזין כתובת URL של תמונה או נתיב קובץ מקומי",
|
||||
"selectLoraRoot": "אנא בחר ספריית שורש של LoRA"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "インポート",
|
||||
"title": "画像またはURLからレシピをインポート",
|
||||
"urlLocalPath": "URL / ローカルパス",
|
||||
"uploadImage": "画像をアップロード",
|
||||
"urlSectionDescription": "Civitai画像URLまたはローカルファイルパスを入力してレシピとしてインポートします。",
|
||||
"dropZoneLabel": "画像をアップロード",
|
||||
"dropZoneHint": "画像をここにドラッグ&ドロップ、クリップボードから貼り付け、またはクリックして参照",
|
||||
"orDivider": "または画像をドラッグ&ドロップ / 貼り付け",
|
||||
"imageUrlOrPath": "画像URLまたはファイルパス:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... または C:/path/to/image.png",
|
||||
"fetchImage": "画像を取得",
|
||||
"uploadSectionDescription": "LoRAメタデータを含む画像をアップロードしてレシピとしてインポートします。",
|
||||
"selectImage": "画像を選択",
|
||||
"recipeName": "レシピ名",
|
||||
"recipeNamePlaceholder": "レシピ名を入力",
|
||||
"tagsOptional": "タグ(任意)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "画像ファイルを選択してください",
|
||||
"enterUrlOrPath": "URLまたはファイルパスを入力してください",
|
||||
"invalidUrl": "有効なURLを入力してください",
|
||||
"invalidInputFormat": "画像のURLまたはローカルの画像ファイルパスを入力してください",
|
||||
"selectLoraRoot": "LoRAルートディレクトリを選択してください"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "가져오기",
|
||||
"title": "이미지 또는 URL에서 레시피 가져오기",
|
||||
"urlLocalPath": "URL / 로컬 경로",
|
||||
"uploadImage": "이미지 업로드",
|
||||
"urlSectionDescription": "Civitai 이미지 URL 또는 로컬 파일 경로를 입력하여 레시피로 가져옵니다.",
|
||||
"dropZoneLabel": "이미지 업로드",
|
||||
"dropZoneHint": "이미지를 여기에 끌어다 놓거나, 클립보드에서 붙여넣거나, 클릭하여 찾아보세요",
|
||||
"orDivider": "또는 이미지를 끌어다 놓기 / 붙여넣기",
|
||||
"imageUrlOrPath": "이미지 URL 또는 파일 경로:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... 또는 C:/path/to/image.png",
|
||||
"fetchImage": "이미지 가져오기",
|
||||
"uploadSectionDescription": "LoRA 메타데이터가 포함된 이미지를 업로드하여 레시피로 가져옵니다.",
|
||||
"selectImage": "이미지 선택",
|
||||
"recipeName": "레시피 이름",
|
||||
"recipeNamePlaceholder": "레시피 이름을 입력하세요",
|
||||
"tagsOptional": "태그 (선택사항)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "이미지 파일을 선택해주세요",
|
||||
"enterUrlOrPath": "URL 또는 파일 경로를 입력해주세요",
|
||||
"invalidUrl": "유효한 URL을 입력하세요",
|
||||
"invalidInputFormat": "이미지 URL 또는 로컬 이미지 파일 경로를 입력하세요",
|
||||
"selectLoraRoot": "LoRA 루트 디렉토리를 선택해주세요"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "Импортировать",
|
||||
"title": "Импортировать рецепт из изображения или URL",
|
||||
"urlLocalPath": "URL / Локальный путь",
|
||||
"uploadImage": "Загрузить изображение",
|
||||
"urlSectionDescription": "Введите URL изображения Civitai или локальный путь к файлу для импорта в качестве рецепта.",
|
||||
"dropZoneLabel": "Загрузить изображение",
|
||||
"dropZoneHint": "Перетащите изображение сюда, вставьте из буфера обмена или нажмите для выбора",
|
||||
"orDivider": "или перетащите / вставьте изображение",
|
||||
"imageUrlOrPath": "URL изображения или путь к файлу:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... или C:/path/to/image.png",
|
||||
"fetchImage": "Получить изображение",
|
||||
"uploadSectionDescription": "Загрузите изображение с метаданными LoRA для импорта в качестве рецепта.",
|
||||
"selectImage": "Выбрать изображение",
|
||||
"recipeName": "Название рецепта",
|
||||
"recipeNamePlaceholder": "Введите название рецепта",
|
||||
"tagsOptional": "Теги (необязательно)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "Пожалуйста, выберите файл изображения",
|
||||
"enterUrlOrPath": "Пожалуйста, введите URL или путь к файлу",
|
||||
"invalidUrl": "Введите корректный URL",
|
||||
"invalidInputFormat": "Введите URL изображения или путь к локальному файлу изображения",
|
||||
"selectLoraRoot": "Пожалуйста, выберите корневую папку LoRA"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "导入",
|
||||
"title": "从图片或 URL 导入配方",
|
||||
"urlLocalPath": "URL / 本地路径",
|
||||
"uploadImage": "上传图片",
|
||||
"urlSectionDescription": "输入来自 civitai.com 或 civitai.red 的 Civitai 图片 URL,或本地文件路径以导入为配方。",
|
||||
"dropZoneLabel": "上传图片",
|
||||
"dropZoneHint": "将图片拖拽到此处、从剪贴板粘贴,或点击浏览",
|
||||
"orDivider": "或拖拽 / 粘贴图片",
|
||||
"imageUrlOrPath": "图片 URL 或文件路径:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... 或 https://civitai.red/images/... 或 C:/path/to/image.png",
|
||||
"fetchImage": "获取图片",
|
||||
"uploadSectionDescription": "上传带有 LoRA 元数据的图片以导入为配方。",
|
||||
"selectImage": "选择图片",
|
||||
"recipeName": "配方名称",
|
||||
"recipeNamePlaceholder": "输入配方名称",
|
||||
"tagsOptional": "标签(可选)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "请选择一个图像文件",
|
||||
"enterUrlOrPath": "请输入 URL 或文件路径",
|
||||
"invalidUrl": "请输入有效的 URL",
|
||||
"invalidInputFormat": "请输入图片 URL 或本地图片文件路径",
|
||||
"selectLoraRoot": "请选择 LoRA 根目录"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -860,14 +860,12 @@
|
||||
"import": {
|
||||
"action": "匯入",
|
||||
"title": "從圖片或網址匯入配方",
|
||||
"urlLocalPath": "網址 / 本機路徑",
|
||||
"uploadImage": "上傳圖片",
|
||||
"urlSectionDescription": "輸入 Civitai 圖片網址或本機檔案路徑以匯入配方。",
|
||||
"dropZoneLabel": "上傳圖片",
|
||||
"dropZoneHint": "將圖片拖曳至此處、從剪貼簿貼上,或點擊瀏覽",
|
||||
"orDivider": "或拖曳 / 貼上圖片",
|
||||
"imageUrlOrPath": "圖片網址或檔案路徑:",
|
||||
"urlPlaceholder": "https://civitai.com/images/... 或 C:/path/to/image.png",
|
||||
"fetchImage": "取得圖片",
|
||||
"uploadSectionDescription": "上傳含 LoRA metadata 的圖片以匯入配方。",
|
||||
"selectImage": "選擇圖片",
|
||||
"recipeName": "配方名稱",
|
||||
"recipeNamePlaceholder": "輸入配方名稱",
|
||||
"tagsOptional": "標籤(選填)",
|
||||
@@ -912,6 +910,8 @@
|
||||
"errors": {
|
||||
"selectImageFile": "請選擇圖片檔案",
|
||||
"enterUrlOrPath": "請輸入網址或檔案路徑",
|
||||
"invalidUrl": "請輸入有效的 URL",
|
||||
"invalidInputFormat": "請輸入圖片 URL 或本機圖片檔案路徑",
|
||||
"selectLoraRoot": "請選擇 LoRA 根目錄"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -77,41 +77,84 @@
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
/* File Input Styles */
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: var(--space-1);
|
||||
.import-description {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-input-button {
|
||||
/* Unified Drop Zone */
|
||||
.import-drop-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-weight: 500;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4) var(--space-3);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-input-button:hover {
|
||||
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||
.import-drop-zone:hover,
|
||||
.import-drop-zone:focus-visible {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover .file-input-button {
|
||||
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||
.import-drop-zone.drag-over {
|
||||
border-color: var(--lora-accent);
|
||||
background: oklch(var(--lora-accent) / 0.08);
|
||||
}
|
||||
|
||||
.drop-zone-icon {
|
||||
font-size: 1.8em;
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.drop-zone-primary {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.drop-zone-filename {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Divider between drop zone and URL input */
|
||||
.import-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: var(--space-3) 0;
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.import-divider::before,
|
||||
.import-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Loading state for the fetch button */
|
||||
#fetchImageBtn.loading {
|
||||
opacity: 0.8;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* Inputs sit flush against the scrollable step's content edge; an outset
|
||||
outline (global offset: 2px) gets clipped by overflow-x. Draw the focus
|
||||
outline inset instead so the full ring stays visible. */
|
||||
#importModal input:focus-visible,
|
||||
#importModal select:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Recipe Details Layout */
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ImportManager {
|
||||
this.selectedFolder = '';
|
||||
this.downloadableLoRAs = [];
|
||||
this.recipeId = null;
|
||||
this.importMode = 'url'; // Default mode: 'url' or 'upload'
|
||||
this.importMode = null; // Set by input handlers: 'url' or 'upload'
|
||||
this.useDefaultPath = false;
|
||||
this.apiClient = null;
|
||||
|
||||
@@ -70,10 +70,8 @@ export class ImportManager {
|
||||
this.stepManager.removeInjectedStyles();
|
||||
});
|
||||
|
||||
// Verify visibility and focus on URL input
|
||||
// Verify visibility and focus on the URL input (primary mode)
|
||||
setTimeout(() => {
|
||||
// Ensure URL option is selected and focus on the input
|
||||
this.toggleImportMode('url');
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
if (urlInput) {
|
||||
urlInput.focus();
|
||||
@@ -87,6 +85,62 @@ export class ImportManager {
|
||||
if (useDefaultPathToggle) {
|
||||
useDefaultPathToggle.addEventListener('change', this.handleToggleDefaultPath);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('importModal');
|
||||
const dropZone = document.getElementById('importDropZone');
|
||||
const fileInput = document.getElementById('recipeImageUpload');
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
|
||||
// Submit URL with Enter
|
||||
if (urlInput) {
|
||||
urlInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.handleUrlInput();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dropZone && fileInput) {
|
||||
// Click or keyboard activation opens the file picker
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
fileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag & drop
|
||||
dropZone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
const file = event.dataTransfer?.files?.[0];
|
||||
if (file) {
|
||||
this.imageProcessor.handleDroppedFile(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Paste an image from clipboard while the modal is open
|
||||
if (modal) {
|
||||
modal.addEventListener('paste', (event) => {
|
||||
if (this.stepManager.currentStep !== 'uploadStep') return;
|
||||
const file = Array.from(event.clipboardData?.files || [])
|
||||
.find(f => f.type.startsWith('image/'));
|
||||
if (file) {
|
||||
event.preventDefault();
|
||||
this.imageProcessor.handleDroppedFile(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetSteps() {
|
||||
@@ -128,9 +182,11 @@ export class ImportManager {
|
||||
this.downloadableLoRAs = [];
|
||||
this.selectedFolder = '';
|
||||
|
||||
// Reset import mode
|
||||
this.importMode = 'url';
|
||||
this.toggleImportMode('url');
|
||||
// Import mode is set by the input handlers ('url' or 'upload')
|
||||
this.importMode = null;
|
||||
|
||||
// Reset drop zone filename feedback
|
||||
this.updateSelectedFileName(null);
|
||||
|
||||
// Clear folder tree selection
|
||||
if (this.folderTreeManager) {
|
||||
@@ -166,43 +222,24 @@ export class ImportManager {
|
||||
}
|
||||
}
|
||||
|
||||
toggleImportMode(mode) {
|
||||
this.importMode = mode;
|
||||
/**
|
||||
* Show the selected file name in the drop zone, or restore the default
|
||||
* hint text when called with null.
|
||||
*/
|
||||
updateSelectedFileName(fileName) {
|
||||
const nameEl = document.getElementById('selectedFileName');
|
||||
const hintEl = document.getElementById('dropZonePrimaryText');
|
||||
if (!nameEl || !hintEl) return;
|
||||
|
||||
// Update toggle buttons
|
||||
const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]');
|
||||
const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]');
|
||||
|
||||
if (uploadBtn && urlBtn) {
|
||||
if (mode === 'upload') {
|
||||
uploadBtn.classList.add('active');
|
||||
urlBtn.classList.remove('active');
|
||||
} else {
|
||||
uploadBtn.classList.remove('active');
|
||||
urlBtn.classList.add('active');
|
||||
}
|
||||
if (fileName) {
|
||||
nameEl.textContent = fileName;
|
||||
nameEl.style.display = 'block';
|
||||
hintEl.style.display = 'none';
|
||||
} else {
|
||||
nameEl.textContent = '';
|
||||
nameEl.style.display = 'none';
|
||||
hintEl.style.display = '';
|
||||
}
|
||||
|
||||
// Show/hide appropriate sections
|
||||
const uploadSection = document.getElementById('uploadSection');
|
||||
const urlSection = document.getElementById('urlSection');
|
||||
|
||||
if (uploadSection && urlSection) {
|
||||
if (mode === 'upload') {
|
||||
uploadSection.style.display = 'block';
|
||||
urlSection.style.display = 'none';
|
||||
} else {
|
||||
uploadSection.style.display = 'none';
|
||||
urlSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Clear error messages
|
||||
const uploadError = document.getElementById('uploadError');
|
||||
const importUrlError = document.getElementById('importUrlError');
|
||||
|
||||
if (uploadError) uploadError.textContent = '';
|
||||
if (importUrlError) importUrlError.textContent = '';
|
||||
}
|
||||
|
||||
handleImageUpload(event) {
|
||||
@@ -345,6 +382,9 @@ export class ImportManager {
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
if (urlInput) urlInput.value = '';
|
||||
|
||||
// Reset drop zone filename feedback
|
||||
this.updateSelectedFileName(null);
|
||||
|
||||
// Clear error messages
|
||||
const uploadError = document.getElementById('uploadError');
|
||||
if (uploadError) uploadError.textContent = '';
|
||||
|
||||
@@ -8,20 +8,32 @@ export class ImageProcessor {
|
||||
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.handleDroppedFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared entry for files coming from the file picker, drag & drop,
|
||||
* or clipboard paste.
|
||||
*/
|
||||
handleDroppedFile(file) {
|
||||
const errorElement = document.getElementById('uploadError');
|
||||
|
||||
if (!file) return;
|
||||
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.match('image.*')) {
|
||||
errorElement.textContent = translate('recipes.controls.import.errors.selectImageFile', {}, 'Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Reset error
|
||||
errorElement.textContent = '';
|
||||
this.importManager.recipeImage = file;
|
||||
|
||||
this.importManager.importMode = 'upload';
|
||||
|
||||
// Show the selected file name in the drop zone
|
||||
this.importManager.updateSelectedFileName(file.name);
|
||||
|
||||
// Auto-proceed to next step if file is selected
|
||||
this.importManager.uploadAndAnalyzeImage();
|
||||
}
|
||||
@@ -30,19 +42,37 @@ export class ImageProcessor {
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
const errorElement = document.getElementById('importUrlError');
|
||||
const input = urlInput.value.trim();
|
||||
|
||||
|
||||
// Validate input
|
||||
if (!input) {
|
||||
errorElement.textContent = translate('recipes.controls.import.errors.enterUrlOrPath', {}, 'Please enter a URL or file path');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Front-end format validation before hitting the backend
|
||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||
try {
|
||||
new URL(input);
|
||||
} catch {
|
||||
errorElement.textContent = translate('recipes.controls.import.errors.invalidUrl', {}, 'Please enter a valid URL');
|
||||
return;
|
||||
}
|
||||
} else if (!/\.(png|jpe?g|webp|gif|bmp|avif|jxl|mp4|webm)$/i.test(input)) {
|
||||
errorElement.textContent = translate('recipes.controls.import.errors.invalidInputFormat', {}, 'Please enter an image URL or a local image file path');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset error
|
||||
errorElement.textContent = '';
|
||||
|
||||
this.importManager.importMode = 'url';
|
||||
|
||||
// Put the fetch button into a loading state to prevent duplicate submits
|
||||
const fetchBtn = document.getElementById('fetchImageBtn');
|
||||
this._setFetchButtonLoading(fetchBtn, true);
|
||||
|
||||
// Show loading indicator
|
||||
this.importManager.loadingManager.showSimpleLoading(translate('recipes.controls.import.processingInput', {}, 'Processing input...'));
|
||||
|
||||
|
||||
try {
|
||||
// Check if it's a URL or a local file path
|
||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||
@@ -55,10 +85,21 @@ export class ImageProcessor {
|
||||
} catch (error) {
|
||||
errorElement.textContent = error.message || 'Failed to process input';
|
||||
} finally {
|
||||
this._setFetchButtonLoading(fetchBtn, false);
|
||||
this.importManager.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_setFetchButtonLoading(button, isLoading) {
|
||||
if (!button) return;
|
||||
button.disabled = isLoading;
|
||||
button.classList.toggle('loading', isLoading);
|
||||
const icon = button.querySelector('i');
|
||||
if (icon) {
|
||||
icon.className = isLoading ? 'fas fa-spinner fa-spin' : 'fas fa-download';
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeImageFromUrl(url) {
|
||||
try {
|
||||
// Call the API with URL data
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export class ImportStepManager {
|
||||
constructor() {
|
||||
this.injectedStyles = null;
|
||||
this.currentStep = null;
|
||||
}
|
||||
|
||||
removeInjectedStyles() {
|
||||
@@ -18,6 +19,7 @@ export class ImportStepManager {
|
||||
showStep(stepId) {
|
||||
// Remove any injected styles to prevent conflicts
|
||||
this.removeInjectedStyles();
|
||||
this.currentStep = stepId;
|
||||
|
||||
// Hide all steps first
|
||||
document.querySelectorAll('.import-step').forEach(step => {
|
||||
|
||||
@@ -5,47 +5,37 @@
|
||||
<h2>{{ t('recipes.controls.import.action') }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Upload Image or Input URL -->
|
||||
<!-- Step 1: Provide Image (URL first, or drop zone below) -->
|
||||
<div class="import-step" id="uploadStep">
|
||||
<div class="import-mode-toggle">
|
||||
<button class="toggle-btn active" data-mode="url" onclick="importManager.toggleImportMode('url')">
|
||||
<i class="fas fa-link"></i> {{ t('recipes.controls.import.urlLocalPath') }}
|
||||
</button>
|
||||
<button class="toggle-btn" data-mode="upload" onclick="importManager.toggleImportMode('upload')">
|
||||
<i class="fas fa-upload"></i> {{ t('recipes.controls.import.uploadImage') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Input URL/Path Section -->
|
||||
<p class="import-description">{{ t('recipes.controls.import.title') }}</p>
|
||||
|
||||
<!-- Input URL/Path Section (primary mode) -->
|
||||
<div class="import-section" id="urlSection">
|
||||
<p>{{ t('recipes.controls.import.urlSectionDescription') }}</p>
|
||||
<div class="input-group">
|
||||
<label for="imageUrlInput">{{ t('recipes.controls.import.imageUrlOrPath') }}</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" id="imageUrlInput" placeholder="{{ t('recipes.controls.import.urlPlaceholder') }}">
|
||||
<button class="primary-btn" onclick="importManager.handleUrlInput()">
|
||||
<button class="primary-btn" id="fetchImageBtn" onclick="importManager.handleUrlInput()">
|
||||
<i class="fas fa-download"></i> {{ t('recipes.controls.import.fetchImage') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="error-message" id="importUrlError"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Image Section -->
|
||||
<div class="import-section" id="uploadSection">
|
||||
<p>{{ t('recipes.controls.import.uploadSectionDescription') }}</p>
|
||||
<div class="input-group">
|
||||
<label for="recipeImageUpload">{{ t('recipes.controls.import.selectImage') }}</label>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
||||
<div class="file-input-button">
|
||||
<i class="fas fa-upload"></i> {{ t('recipes.controls.import.selectImage') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-message" id="uploadError"></div>
|
||||
</div>
|
||||
|
||||
<div class="import-divider"><span>{{ t('recipes.controls.import.orDivider') }}</span></div>
|
||||
|
||||
<!-- Unified drop zone: click to browse, drag & drop, or paste an image -->
|
||||
<div class="import-drop-zone" id="importDropZone" tabindex="0" role="button"
|
||||
aria-label="{{ t('recipes.controls.import.dropZoneLabel') }}">
|
||||
<input type="file" id="recipeImageUpload" accept="image/*" hidden
|
||||
onchange="importManager.handleImageUpload(event)">
|
||||
<i class="fas fa-cloud-upload-alt drop-zone-icon"></i>
|
||||
<p class="drop-zone-primary" id="dropZonePrimaryText">{{ t('recipes.controls.import.dropZoneHint') }}</p>
|
||||
<p class="drop-zone-filename" id="selectedFileName" style="display: none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="uploadError"></div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">{{ t('common.actions.cancel') }}</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user