diff --git a/locales/de.json b/locales/de.json index 5260ce85..c833c968 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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" } }, diff --git a/locales/en.json b/locales/en.json index d568cbcf..f64baca9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" } }, diff --git a/locales/es.json b/locales/es.json index b2799a92..768692b8 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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" } }, diff --git a/locales/fr.json b/locales/fr.json index 668a5156..4be323bf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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" } }, diff --git a/locales/he.json b/locales/he.json index e8caaf1d..ed544a0d 100644 --- a/locales/he.json +++ b/locales/he.json @@ -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" } }, diff --git a/locales/ja.json b/locales/ja.json index f85e66e9..30ddca46 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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ルートディレクトリを選択してください" } }, diff --git a/locales/ko.json b/locales/ko.json index 9e378425..cbc15b72 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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 루트 디렉토리를 선택해주세요" } }, diff --git a/locales/ru.json b/locales/ru.json index 07853a05..822d09ee 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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" } }, diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 474b0f40..da9f30a8 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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 根目录" } }, diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 59bd3f86..bfa354ca 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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 根目錄" } }, diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index c228728d..329b1d89 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -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 */ diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index 435debd9..7bdce97c 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -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 = ''; diff --git a/static/js/managers/import/ImageProcessor.js b/static/js/managers/import/ImageProcessor.js index a3b62c2b..541f7d79 100644 --- a/static/js/managers/import/ImageProcessor.js +++ b/static/js/managers/import/ImageProcessor.js @@ -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 diff --git a/static/js/managers/import/ImportStepManager.js b/static/js/managers/import/ImportStepManager.js index d5b8e47d..af2ae599 100644 --- a/static/js/managers/import/ImportStepManager.js +++ b/static/js/managers/import/ImportStepManager.js @@ -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 => { diff --git a/templates/components/import_modal.html b/templates/components/import_modal.html index 008db05f..a3594202 100644 --- a/templates/components/import_modal.html +++ b/templates/components/import_modal.html @@ -5,47 +5,37 @@
{{ t('recipes.controls.import.title') }}
+ +{{ t('recipes.controls.import.urlSectionDescription') }}
{{ t('recipes.controls.import.uploadSectionDescription') }}
-{{ t('recipes.controls.import.dropZoneHint') }}
+