Compare commits

...

5 Commits

25 changed files with 1311 additions and 1642 deletions
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 根目錄"
}
},
+68 -25
View File
@@ -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 */
+1 -1
View File
@@ -1595,7 +1595,7 @@ class RecipeModal {
let headerAction = '';
if (existsLocally && localPath) {
headerAction = `
<button class="resource-action compact checkpoint-send">
<button class="resource-action primary compact checkpoint-send">
<i class="fas fa-paper-plane"></i>
<span>${translate('recipes.actions.sendCheckpoint', {}, 'Send to ComfyUI')}</span>
</button>
+82 -42
View File
@@ -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 = '';
+3
View File
@@ -904,6 +904,9 @@ export class SettingsManager {
// Helper to update model Combobox presets from catalog / Ollama API
const llmModelInput = document.getElementById('llmModel');
this._llmModelCombobox = null;
if (llmModelInput) {
llmModelInput.value = state.global.settings.llm_model || '';
}
if (llmModelInput && typeof Combobox !== 'undefined') {
const currentProvider = llmProviderSelect ? llmProviderSelect.value : 'openai';
const fallbackModels = currentProvider === 'ollama' ? [] : (this._providerModels[currentProvider] || []);
+50 -9
View File
@@ -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 => {
+18 -28
View File
@@ -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>
@@ -0,0 +1,69 @@
{# Shared building blocks for the settings modal sections. #}
{# Usage: {% import 'components/modals/settings/_macros.html' as sm with context %} #}
{# `with context` is required so macros can call the `t()` translation function. #}
{% macro setting_toggle(id, key, label, help='') %}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="{{ id }}">
{{ t(label) }}
{% if help %}<i class="fas fa-info-circle info-icon" data-tooltip="{{ t(help) }}"></i>{% endif %}
</label>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="{{ id }}" onchange="settingsManager.saveToggleSetting('{{ id }}', '{{ key }}')">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
{% endmacro %}
{% macro setting_select(id, key, label, options, help='') %}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="{{ id }}">
{{ t(label) }}
{% if help %}<i class="fas fa-info-circle info-icon" data-tooltip="{{ t(help) }}"></i>{% endif %}
</label>
</div>
<div class="setting-control select-control">
<select id="{{ id }}" onchange="settingsManager.saveSelectSetting('{{ id }}', '{{ key }}')">
{% for value, option_label in options %}
<option value="{{ value }}">{{ t(option_label) }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
{% endmacro %}
{% macro setting_input(id, key, label, placeholder, help='') %}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="{{ id }}">
{{ t(label) }}
{% if help %}<i class="fas fa-info-circle info-icon" data-tooltip="{{ t(help) }}"></i>{% endif %}
</label>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input type="text" id="{{ id }}"
placeholder="{{ t(placeholder) }}"
onblur="settingsManager.saveInputSetting('{{ id }}', '{{ key }}')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro subsection_header(title) %}
<div class="settings-subsection-header">
<h4>{{ t(title) }}</h4>
</div>
{% endmacro %}
@@ -0,0 +1,326 @@
{% import 'components/modals/settings/_macros.html' as sm with context %}
<!-- Section 1: General -->
<div id="section-general" class="settings-section active" data-section="general">
<!-- Language -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="languageSelect">
{{ t('common.language.select') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('common.language.select_help') }}"></i>
</label>
</div>
<div class="setting-control select-control">
<select id="languageSelect" onchange="settingsManager.saveLanguageSetting()">
<option value="en">{{ t('common.language.english') }}</option>
<option value="zh-CN">{{ t('common.language.chinese_simplified') }}</option>
<option value="zh-TW">{{ t('common.language.chinese_traditional') }}</option>
<option value="ru">{{ t('common.language.russian') }}</option>
<option value="de">{{ t('common.language.german') }}</option>
<option value="ja">{{ t('common.language.japanese') }}</option>
<option value="ko">{{ t('common.language.korean') }}</option>
<option value="fr">{{ t('common.language.french') }}</option>
<option value="es">{{ t('common.language.spanish') }}</option>
<option value="he">{{ t('common.language.Hebrew') }}</option>
</select>
</div>
</div>
</div>
<!-- Storage Location -->
{{ sm.setting_toggle('usePortableSettings', 'use_portable_settings', 'settings.storage.locationLabel', 'settings.storage.locationHelp') }}
<!-- API Configuration -->
<div class="setting-item api-key-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.civitaiApiKey') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.civitaiApiKeyHelp') }}"></i>
</div>
<div class="setting-control">
<!-- Status display (shown when not editing) -->
<div id="civitaiApiKeyStatus" class="api-key-status">
<span id="civitaiApiKeyStatusText" class="api-key-status-text api-key-status--unconfigured">
<i class="fas fa-times-circle text-error"></i>
{{ t('settings.civitaiApiKeyNotConfigured') }}
</span>
<button type="button" class="secondary-btn" id="civitaiApiKeyActionBtn" onclick="settingsManager.editApiKey()">
{{ t('settings.civitaiApiKeySet') }}
</button>
</div>
<!-- Inline edit view (shown when editing) -->
<div id="civitaiApiKeyEdit" class="api-key-edit is-hidden">
<div class="api-key-input">
<input type="text"
id="civitaiApiKey"
class="api-key-masked"
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}"
autocomplete="off"
data-mask="css" />
<button type="button" class="toggle-visibility">
<i class="fas fa-eye"></i>
</button>
</div>
<button type="button" class="primary-btn" onclick="settingsManager.saveApiKey()">{{ t('common.actions.save') }}</button>
<button type="button" class="secondary-btn" onclick="settingsManager.cancelEditApiKey()">{{ t('common.actions.cancel') }}</button>
</div>
</div>
</div>
</div>
{{ sm.setting_select('civitaiHost', 'civitai_host', 'settings.civitaiHost.label', [
('civitai.com', 'settings.civitaiHost.options.com'),
('civitai.red', 'settings.civitaiHost.options.red'),
], 'settings.civitaiHost.help') }}
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.downloads') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="downloadBackend">{{ t('settings.downloadBackend.label') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.downloadBackend.help') }}"></i>
<a class="settings-action-link" href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Aria2-Download-Backend-(Experimental)" target="_blank" rel="noopener" aria-label="{{ t('settings.aria2HelpLink') }}" title="{{ t('settings.aria2HelpLink') }}">
<i class="fas fa-question-circle" aria-hidden="true"></i>
</a>
</div>
<div class="setting-control select-control">
<select id="downloadBackend" onchange="settingsManager.saveSelectSetting('downloadBackend', 'download_backend')">
<option value="python">{{ t('settings.downloadBackend.options.python') }}</option>
<option value="aria2">{{ t('settings.downloadBackend.options.aria2') }}</option>
</select>
</div>
</div>
</div>
<div class="setting-item" id="aria2PathSetting" style="display: none;">
<div class="setting-row">
<div class="setting-info">
<label for="aria2cPath">{{ t('settings.aria2cPath.label') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.aria2cPath.help') }}"></i>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input type="text"
id="aria2cPath"
placeholder="{{ t('settings.aria2cPath.placeholder') }}"
onblur="settingsManager.saveInputSetting('aria2cPath', 'aria2c_path')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
</div>
</div>
<!-- AI Provider Configuration (BYOK) -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.aiProvider.title') }}
{{ sm.setting_select('llmProvider', 'llm_provider', 'settings.aiProvider.provider', [
('openai', 'settings.aiProvider.providerOptions.openai'),
('ollama', 'settings.aiProvider.providerOptions.ollama'),
('deepseek', 'settings.aiProvider.providerOptions.deepseek'),
('groq', 'settings.aiProvider.providerOptions.groq'),
('openrouter', 'settings.aiProvider.providerOptions.openrouter'),
('google', 'settings.aiProvider.providerOptions.google'),
('opencode-go', 'settings.aiProvider.providerOptions.opencode-go'),
('custom', 'settings.aiProvider.providerOptions.custom'),
], 'settings.aiProvider.providerHelp') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="llmApiBase">{{ t('settings.aiProvider.apiBase') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.aiProvider.apiBaseHelp') }}"></i>
</div>
<div class="setting-control">
<div class="text-input-wrapper lm-combobox-container">
<input type="text" id="llmApiBase"
class="lm-combobox-input"
placeholder="{{ t('settings.aiProvider.apiBasePlaceholder') }}"
autocomplete="off"
onblur="settingsManager.saveInputSetting('llmApiBase', 'llm_api_base')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
</div>
<div class="setting-item api-key-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.aiProvider.apiKey') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.aiProvider.apiKeyHelp') }}"></i>
</div>
<div class="setting-control">
<div id="llmApiKeyStatus" class="api-key-status">
<span id="llmApiKeyStatusText" class="api-key-status-text api-key-status--unconfigured">
<i class="fas fa-times-circle text-error"></i>
{{ t('settings.aiProvider.apiKeyNotSet') }}
</span>
<button type="button" class="secondary-btn" id="llmApiKeyActionBtn" onclick="settingsManager.editApiKey('llm_api_key', 'llmApiKey')">
{{ t('settings.aiProvider.apiKeySet') }}
</button>
</div>
<div id="llmApiKeyEdit" class="api-key-edit is-hidden">
<div class="api-key-input">
<input type="text"
id="llmApiKey"
class="api-key-masked"
placeholder="{{ t('settings.aiProvider.apiKeyPlaceholder') }}"
autocomplete="off"
data-mask="css" />
<button type="button" class="toggle-visibility">
<i class="fas fa-eye"></i>
</button>
</div>
<button type="button" class="primary-btn" onclick="settingsManager.saveApiKey('llm_api_key', 'llmApiKey')">{{ t('common.actions.save') }}</button>
<button type="button" class="secondary-btn" onclick="settingsManager.cancelEditApiKey(true, 'llmApiKey')">{{ t('common.actions.cancel') }}</button>
</div>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="llmModel">{{ t('settings.aiProvider.model') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.aiProvider.modelHelp') }}"></i>
</div>
<div class="setting-control">
<div class="text-input-wrapper lm-combobox-container">
<input type="text" id="llmModel"
class="lm-combobox-input"
placeholder="{{ t('settings.aiProvider.modelPlaceholder') }}"
autocomplete="off"
onblur="settingsManager.saveInputSetting('llmModel', 'llm_model')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
</div>
</div>
<!-- Backup -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.backup') }}
<div class="settings-help-text subtle">
{{ t('settings.backup.scopeHelp') }}
</div>
{{ sm.setting_toggle('backupAutoEnabled', 'backup_auto_enabled', 'settings.backup.autoEnabled', 'settings.backup.autoEnabledHelp') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="backupRetentionCount">
{{ t('settings.backup.retention') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.backup.retentionHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input
type="number"
id="backupRetentionCount"
min="1"
step="1"
onblur="settingsManager.saveInputSetting('backupRetentionCount', 'backup_retention_count')"
onkeydown="if(event.key === 'Enter') { this.blur(); }"
/>
</div>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>
{{ t('settings.backup.management') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.backup.managementHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<button type="button" class="secondary-btn" onclick="settingsManager.exportBackup()">
{{ t('settings.backup.exportButton') }}
</button>
<button type="button" class="secondary-btn" onclick="settingsManager.triggerBackupImport()" style="margin-left: 10px;">
{{ t('settings.backup.importButton') }}
</button>
<input
type="file"
id="backupImportInput"
accept=".zip,application/zip"
style="display: none;"
onchange="settingsManager.handleBackupImportFile(this)"
/>
</div>
</div>
</div>
<div class="setting-item">
<details class="backup-location-details">
<summary>{{ t('settings.backup.locationSummary') }}</summary>
<div class="backup-location-panel">
<code id="backupLocationPath" class="backup-location-path"></code>
<button type="button" class="secondary-btn" id="backupOpenLocationBtn">
{{ t('settings.backup.openFolderButton') }}
</button>
</div>
</details>
</div>
<div class="setting-item">
<div class="backup-status" id="backupStatus">
<!-- Status will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Proxy Settings -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.proxySettings') }}
{{ sm.setting_toggle('proxyEnabled', 'proxy_enabled', 'settings.proxySettings.enableProxy', 'settings.proxySettings.enableProxyHelp') }}
<div id="proxySettingsGroup" class="proxy-settings-group" style="display: none;">
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyType">
{{ t('settings.proxySettings.proxyType') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.proxySettings.proxyTypeHelp') }}"></i>
</label>
</div>
<div class="setting-control select-control">
<select id="proxyType" onchange="settingsManager.saveSelectSetting('proxyType', 'proxy_type')">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks4">SOCKS4</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
</div>
</div>
{{ sm.setting_input('proxyHost', 'proxy_host', 'settings.proxySettings.proxyHost', 'settings.proxySettings.proxyHostPlaceholder', 'settings.proxySettings.proxyHostHelp') }}
{{ sm.setting_input('proxyPort', 'proxy_port', 'settings.proxySettings.proxyPort', 'settings.proxySettings.proxyPortPlaceholder', 'settings.proxySettings.proxyPortHelp') }}
{{ sm.setting_input('proxyUsername', 'proxy_username', 'settings.proxySettings.proxyUsername', 'settings.proxySettings.proxyUsernamePlaceholder', 'settings.proxySettings.proxyUsernameHelp') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyPassword">
{{ t('settings.proxySettings.proxyPassword') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.proxySettings.proxyPasswordHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<div class="api-key-input">
<input type="password" id="proxyPassword"
placeholder="{{ t('settings.proxySettings.proxyPasswordPlaceholder') }}"
autocomplete="new-password"
onblur="settingsManager.saveInputSetting('proxyPassword', 'proxy_password')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
<button class="toggle-visibility">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,127 @@
{% import 'components/modals/settings/_macros.html' as sm with context %}
<!-- Section 2: Interface -->
<div id="section-interface" class="settings-section" data-section="interface">
<!-- Content Filtering -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.contentFiltering') }}
{{ sm.setting_toggle('blurMatureContent', 'blur_mature_content', 'settings.contentFiltering.blurNsfwContent', 'settings.contentFiltering.blurNsfwContentHelp') }}
{{ sm.setting_toggle('showOnlySFW', 'show_only_sfw', 'settings.contentFiltering.showOnlySfw', 'settings.contentFiltering.showOnlySfwHelp') }}
{{ sm.setting_select('matureBlurLevel', 'mature_blur_level', 'settings.contentFiltering.matureBlurThreshold', [
('PG13', 'settings.contentFiltering.matureBlurThresholdOptions.pg13'),
('R', 'settings.contentFiltering.matureBlurThresholdOptions.r'),
('X', 'settings.contentFiltering.matureBlurThresholdOptions.x'),
('XXX', 'settings.contentFiltering.matureBlurThresholdOptions.xxx'),
], 'settings.contentFiltering.matureBlurThresholdHelp') }}
</div>
<!-- Video Settings -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.videoSettings') }}
{{ sm.setting_toggle('autoplayOnHover', 'autoplay_on_hover', 'settings.videoSettings.autoplayOnHover', 'settings.videoSettings.autoplayOnHoverHelp') }}
</div>
<!-- Layout Settings -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.layoutSettings') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="displayDensity">
{{ t('settings.layoutSettings.displayDensity') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.layoutSettings.displayDensityHelp') }}"></i>
</label>
</div>
<div class="setting-control select-control">
<select id="displayDensity" onchange="settingsManager.saveSelectSetting('displayDensity', 'display_density')">
<option value="default">{{ t('settings.layoutSettings.displayDensityOptions.default') }}</option>
<option value="medium">{{ t('settings.layoutSettings.displayDensityOptions.medium') }}</option>
<option value="compact">{{ t('settings.layoutSettings.displayDensityOptions.compact') }}</option>
</select>
</div>
</div>
<div class="input-help"><ul class="list-description">
<li><strong>{{ t('settings.layoutSettings.displayDensityOptions.default') }}:</strong> {{ t('settings.layoutSettings.displayDensityDetails.default') }}</li>
<li><strong>{{ t('settings.layoutSettings.displayDensityOptions.medium') }}:</strong> {{ t('settings.layoutSettings.displayDensityDetails.medium') }}</li>
<li><strong>{{ t('settings.layoutSettings.displayDensityOptions.compact') }}:</strong> {{ t('settings.layoutSettings.displayDensityDetails.compact') }}</li>
</ul>
<span class="warning-text">{{ t('settings.layoutSettings.displayDensityWarning') }}</span>
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label id="recipesLayoutLabel">
{{ t('settings.layoutSettings.recipesLayout') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.layoutSettings.recipesLayoutHelp') }}"></i>
</label>
</div>
<div class="setting-control layout-options-control">
<div id="recipesLayoutOptions" class="layout-options" role="radiogroup" aria-label="{{ t('settings.layoutSettings.recipesLayout') }}" aria-labelledby="recipesLayoutLabel">
<button type="button" class="layout-option" data-recipes-layout="grid" onclick="settingsManager.saveRecipesLayout('grid')" role="radio" aria-checked="true">
<span class="layout-option-preview layout-preview-grid" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
<span class="layout-option-label">{{ t('settings.layoutSettings.recipesLayoutOptions.grid') }}</span>
</button>
<button type="button" class="layout-option" data-recipes-layout="masonry" onclick="settingsManager.saveRecipesLayout('masonry')" role="radio" aria-checked="false">
<span class="layout-option-preview layout-preview-masonry" aria-hidden="true"><span></span><span></span><span></span></span>
<span class="layout-option-label">{{ t('settings.layoutSettings.recipesLayoutOptions.masonry') }}</span>
</button>
</div>
</div>
</div>
</div>
{{ sm.setting_select('modelNameDisplay', 'model_name_display', 'settings.layoutSettings.modelNameDisplay', [
('model_name', 'settings.layoutSettings.modelNameDisplayOptions.modelName'),
('file_name', 'settings.layoutSettings.modelNameDisplayOptions.fileName'),
], 'settings.layoutSettings.modelNameDisplayHelp') }}
<!-- Group by model toggle -->
{{ sm.setting_toggle('groupByModel', 'group_by_model', 'settings.layoutSettings.groupByModel', 'settings.layoutSettings.groupByModelHelp') }}
{{ sm.setting_select('cardInfoDisplay', 'card_info_display', 'settings.layoutSettings.cardInfoDisplay', [
('always', 'settings.layoutSettings.cardInfoDisplayOptions.always'),
('hover', 'settings.layoutSettings.cardInfoDisplayOptions.hover'),
], 'settings.layoutSettings.cardInfoDisplayHelp') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="cardBlurAmount">
{{ t('settings.layoutSettings.cardBlurAmount') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.layoutSettings.cardBlurAmountHelp') }}"></i>
</label>
</div>
<div class="setting-control range-control">
<input type="range" id="cardBlurAmount" min="0" max="20" value="8" step="1"
oninput="var pct = (this.value / 20) * 100; this.style.setProperty('--range-fill', pct + '%'); document.getElementById('cardBlurAmountValue').textContent = this.value + 'px'"
onchange="settingsManager.saveRangeSetting('cardBlurAmount', 'cardBlurAmountValue', 'card_blur_amount')">
<span id="cardBlurAmountValue" class="range-value">8px</span>
</div>
</div>
</div>
{{ sm.setting_toggle('showVersionOnCard', 'show_version_on_card', 'settings.layoutSettings.showVersionOnCard', 'settings.layoutSettings.showVersionOnCardHelp') }}
{{ sm.setting_select('modelCardFooterAction', 'model_card_footer_action', 'settings.layoutSettings.modelCardFooterAction', [
('example_images', 'settings.layoutSettings.modelCardFooterActionOptions.exampleImages'),
('replace_preview', 'settings.layoutSettings.modelCardFooterActionOptions.replacePreview'),
], 'settings.layoutSettings.modelCardFooterActionHelp') }}
</div>
<!-- License Icons -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.licenseIcons') }}
{{ sm.setting_toggle('useNewLicenseIcons', 'use_new_license_icons', 'settings.licenseIcons.useNewStyle', 'settings.licenseIcons.useNewStyleHelp') }}
</div>
<!-- Miscellaneous -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.misc') }}
{{ sm.setting_select('loraSyntaxFormat', 'lora_syntax_format', 'settings.misc.loraSyntaxFormat', [
('full', 'settings.misc.loraSyntaxFormatOptions.full'),
('legacy', 'settings.misc.loraSyntaxFormatOptions.legacy'),
], 'settings.misc.loraSyntaxFormatHelp') }}
{{ sm.setting_toggle('includeTriggerWords', 'include_trigger_words', 'settings.misc.includeTriggerWords', 'settings.misc.includeTriggerWordsHelp') }}
</div>
</div>
@@ -0,0 +1,482 @@
{% import 'components/modals/settings/_macros.html' as sm with context %}
{% set template_preset_options = [
('', 'settings.downloadPathTemplates.templateOptions.flatStructure'),
('{base_model}', 'settings.downloadPathTemplates.templateOptions.byBaseModel'),
('{author}', 'settings.downloadPathTemplates.templateOptions.byAuthor'),
('{first_tag}', 'settings.downloadPathTemplates.templateOptions.byFirstTag'),
('{base_model}/{first_tag}', 'settings.downloadPathTemplates.templateOptions.baseModelFirstTag'),
('{base_model}/{author}', 'settings.downloadPathTemplates.templateOptions.baseModelAuthor'),
('{author}/{first_tag}', 'settings.downloadPathTemplates.templateOptions.authorFirstTag'),
('{base_model}/{author}/{first_tag}', 'settings.downloadPathTemplates.templateOptions.baseModelAuthorFirstTag'),
('custom', 'settings.downloadPathTemplates.templateOptions.customTemplate'),
] %}
<!-- Section 3: Library -->
<div id="section-library" class="settings-section" data-section="library">
<!-- Folder Settings -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.folderSettings') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="librarySelect">
{{ t('settings.folderSettings.activeLibrary') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.folderSettings.activeLibraryHelp') }}"></i>
</label>
</div>
<div class="setting-control select-control">
<select id="librarySelect" onchange="settingsManager.handleLibraryChange()">
<option value="">{{ t('settings.folderSettings.loadingLibraries') }}</option>
</select>
</div>
</div>
</div>
{{ sm.setting_select('defaultLoraRoot', 'default_lora_root', 'settings.folderSettings.defaultLoraRoot', [], 'settings.folderSettings.defaultLoraRootHelp') }}
{{ sm.setting_select('defaultCheckpointRoot', 'default_checkpoint_root', 'settings.folderSettings.defaultCheckpointRoot', [], 'settings.folderSettings.defaultCheckpointRootHelp') }}
{{ sm.setting_select('defaultUnetRoot', 'default_unet_root', 'settings.folderSettings.defaultUnetRoot', [], 'settings.folderSettings.defaultUnetRootHelp') }}
{{ sm.setting_select('defaultEmbeddingRoot', 'default_embedding_root', 'settings.folderSettings.defaultEmbeddingRoot', [], 'settings.folderSettings.defaultEmbeddingRootHelp') }}
</div>
<!-- Recipe Settings -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.recipeSettings') }}
{{ sm.setting_input('recipesPath', 'recipes_path', 'settings.folderSettings.recipesPath', 'settings.folderSettings.recipesPathPlaceholder', 'settings.folderSettings.recipesPathHelp') }}
</div>
<!-- Extra Folder Paths -->
<div class="settings-subsection">
<div class="settings-subsection-header">
<h4>
{{ t('settings.extraFolderPaths.title') }}
<i class="fas fa-sync-alt restart-required-icon" title="{{ t('settings.extraFolderPaths.restartRequired') }}"></i>
</h4>
</div>
<div class="setting-item">
<div class="input-help">
{{ t('settings.extraFolderPaths.description') }}
</div>
</div>
<!-- LoRA Paths -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.extraFolderPaths.modelTypes.lora') }}</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addExtraFolderPathRow('loras')">
<i class="fas fa-plus"></i>
<span>{{ t('common.actions.add') }}</span>
</button>
</div>
</div>
<div class="extra-folder-paths-container" id="extraFolderPaths-loras">
</div>
</div>
<!-- Checkpoint Paths -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.extraFolderPaths.modelTypes.checkpoint') }}</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addExtraFolderPathRow('checkpoints')">
<i class="fas fa-plus"></i>
<span>{{ t('common.actions.add') }}</span>
</button>
</div>
</div>
<div class="extra-folder-paths-container" id="extraFolderPaths-checkpoints">
</div>
</div>
<!-- Diffusion Model (Unet) Paths -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.extraFolderPaths.modelTypes.unet') }}</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addExtraFolderPathRow('unet')">
<i class="fas fa-plus"></i>
<span>{{ t('common.actions.add') }}</span>
</button>
</div>
</div>
<div class="extra-folder-paths-container" id="extraFolderPaths-unet">
</div>
</div>
<!-- Embedding Paths -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>{{ t('settings.extraFolderPaths.modelTypes.embedding') }}</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addExtraFolderPathRow('embeddings')">
<i class="fas fa-plus"></i>
<span>{{ t('common.actions.add') }}</span>
</button>
</div>
</div>
<div class="extra-folder-paths-container" id="extraFolderPaths-embeddings">
</div>
</div>
</div>
<!-- Download Path Templates -->
<div class="settings-subsection">
<div class="settings-subsection-header">
<h4>
{{ t('settings.downloadPathTemplates.title') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.downloadPathTemplates.help') }}"></i>
</h4>
</div>
<div class="setting-item">
<div class="input-help">
<div class="placeholder-info">
<strong>{{ t('settings.downloadPathTemplates.availablePlaceholders') }}</strong>
<span class="placeholder-tag">{base_model}</span>
<span class="placeholder-tag">{author}</span>
<span class="placeholder-tag">{first_tag}</span>
<span class="placeholder-tag">{model_name}</span>
<span class="placeholder-tag">{version_name}</span>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="loraTemplatePreset">{{ t('settings.downloadPathTemplates.modelTypes.lora') }}</label>
</div>
<div class="setting-control select-control">
<select id="loraTemplatePreset" onchange="settingsManager.updateTemplatePreset('lora', this.value)">
{% for value, option_label in template_preset_options %}
<option value="{{ value }}">{{ t(option_label) }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="template-custom-row" id="loraCustomRow" style="display: none;">
<input type="text" id="loraCustomTemplate" class="template-custom-input" placeholder="{{ t('settings.downloadPathTemplates.customTemplatePlaceholder') }}" />
<div class="template-validation" id="loraValidation"></div>
</div>
<div class="template-preview" id="loraPreview"></div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="checkpointTemplatePreset">{{ t('settings.downloadPathTemplates.modelTypes.checkpoint') }}</label>
</div>
<div class="setting-control select-control">
<select id="checkpointTemplatePreset" onchange="settingsManager.updateTemplatePreset('checkpoint', this.value)">
{% for value, option_label in template_preset_options %}
<option value="{{ value }}">{{ t(option_label) }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="template-custom-row" id="checkpointCustomRow" style="display: none;">
<input type="text" id="checkpointCustomTemplate" class="template-custom-input" placeholder="{{ t('settings.downloadPathTemplates.customTemplatePlaceholder') }}" />
<div class="template-validation" id="checkpointValidation"></div>
</div>
<div class="template-preview" id="checkpointPreview"></div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="embeddingTemplatePreset">{{ t('settings.downloadPathTemplates.modelTypes.embedding') }}</label>
</div>
<div class="setting-control select-control">
<select id="embeddingTemplatePreset" onchange="settingsManager.updateTemplatePreset('embedding', this.value)">
{% for value, option_label in template_preset_options %}
<option value="{{ value }}">{{ t(option_label) }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="template-custom-row" id="embeddingCustomRow" style="display: none;">
<input type="text" id="embeddingCustomTemplate" class="template-custom-input" placeholder="{{ t('settings.downloadPathTemplates.customTemplatePlaceholder') }}" />
<div class="template-validation" id="embeddingValidation"></div>
</div>
<div class="template-preview" id="embeddingPreview"></div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>
{{ t('settings.downloadPathTemplates.baseModelPathMappings') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.downloadPathTemplates.baseModelPathMappingsHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addMappingRow()">
<i class="fas fa-plus"></i>
<span>{{ t('settings.downloadPathTemplates.addMapping') }}</span>
</button>
</div>
</div>
<div class="mappings-container">
<div id="baseModelMappingsContainer">
</div>
</div>
</div>
{{ sm.setting_toggle('skipPreviouslyDownloadedModelVersions', 'skip_previously_downloaded_model_versions', 'settings.skipPreviouslyDownloadedModelVersions.label', 'settings.skipPreviouslyDownloadedModelVersions.help') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="downloadSkipBaseModelsToggle">
{{ t('settings.downloadSkipBaseModels.label') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.downloadSkipBaseModels.help') }}"></i>
</label>
</div>
<div class="setting-control">
<button
type="button"
id="downloadSkipBaseModelsToggle"
class="secondary-btn base-model-skip-toggle"
aria-expanded="false"
>
<span id="downloadSkipBaseModelsSummary">{{ t('settings.downloadSkipBaseModels.summary.none') }}</span>
<span class="base-model-skip-toggle-label">{{ t('settings.downloadSkipBaseModels.actions.edit') }}</span>
</button>
</div>
</div>
<div id="downloadSkipBaseModelsPanel" class="base-model-skip-panel" hidden>
<div class="base-model-skip-toolbar">
<input
type="text"
id="downloadSkipBaseModelsSearch"
class="base-model-skip-search"
placeholder="{{ t('settings.downloadSkipBaseModels.searchPlaceholder') }}"
/>
<button type="button" class="text-btn base-model-skip-clear" id="downloadSkipBaseModelsClear">
{{ t('settings.downloadSkipBaseModels.actions.clear') }}
</button>
</div>
<div id="downloadSkipBaseModelsContainer" class="base-model-skip-list"></div>
<div id="downloadSkipBaseModelsEmpty" class="base-model-skip-empty" hidden>
{{ t('settings.downloadSkipBaseModels.empty') }}
</div>
</div>
<div class="settings-input-error-message" id="downloadSkipBaseModelsError"></div>
</div>
<!-- Priority Tags -->
<div class="setting-item priority-tags-item">
<div class="setting-row priority-tags-header-row">
<div class="setting-info priority-tags-header">
<label>
{{ t('settings.priorityTags.title') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.priorityTags.description') }}"></i>
</label>
<a class="settings-action-link priority-tags-help-link" href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Priority-Tags-Configuration-Guide" target="_blank" rel="noopener" aria-label="{{ t('settings.priorityTags.helpLinkLabel') }}" title="{{ t('settings.priorityTags.helpLinkLabel') }}">
<i class="fas fa-question-circle" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="priority-tags-tabs">
<input type="radio" id="priority-tags-tab-lora" name="priority-tags-tab" class="priority-tags-tab-input" checked>
<input type="radio" id="priority-tags-tab-checkpoint" name="priority-tags-tab" class="priority-tags-tab-input">
<input type="radio" id="priority-tags-tab-embedding" name="priority-tags-tab" class="priority-tags-tab-input">
<div class="priority-tags-tablist">
<label class="priority-tags-tab-label" for="priority-tags-tab-lora" id="priority-tags-tab-lora-label">{{ t('settings.priorityTags.modelTypes.lora') }}</label>
<label class="priority-tags-tab-label" for="priority-tags-tab-checkpoint" id="priority-tags-tab-checkpoint-label">{{ t('settings.priorityTags.modelTypes.checkpoint') }}</label>
<label class="priority-tags-tab-label" for="priority-tags-tab-embedding" id="priority-tags-tab-embedding-label">{{ t('settings.priorityTags.modelTypes.embedding') }}</label>
</div>
<div class="priority-tags-panels">
<div class="priority-tags-panel" id="priority-tags-panel-lora">
<textarea id="loraPriorityTagsInput" class="priority-tags-input" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
<div class="settings-input-error-message" id="loraPriorityTagsError"></div>
</div>
<div class="priority-tags-panel" id="priority-tags-panel-checkpoint">
<textarea id="checkpointPriorityTagsInput" class="priority-tags-input" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
<div class="settings-input-error-message" id="checkpointPriorityTagsError"></div>
</div>
<div class="priority-tags-panel" id="priority-tags-panel-embedding">
<textarea id="embeddingPriorityTagsInput" class="priority-tags-input" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
<div class="settings-input-error-message" id="embeddingPriorityTagsError"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Version Scope -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.versionScope') }}
{{ sm.setting_select('versionGrouping', 'version_grouping', 'settings.versionGrouping.label', [
('same_base', 'settings.versionGrouping.options.sameBase'),
('any', 'settings.versionGrouping.options.any'),
], 'settings.versionGrouping.help') }}
{{ sm.setting_toggle('hideEarlyAccessUpdates', 'hide_early_access_updates', 'settings.hideEarlyAccessUpdates.label', 'settings.hideEarlyAccessUpdates.help') }}
{{ sm.setting_toggle('hidePaidUpdates', 'hide_paid_updates', 'settings.hidePaidUpdates.label', 'settings.hidePaidUpdates.help') }}
</div>
<!-- Example Images -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.exampleImages') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="exampleImagesPath">{{ t('settings.exampleImages.downloadLocation') }} <i class="fas fa-sync-alt restart-required-icon" title="{{ t('settings.exampleImages.restartRequired') }}"></i></label>
</div>
<div class="setting-control path-control">
<input type="text" id="exampleImagesPath" placeholder="{{ t('settings.exampleImages.downloadLocationPlaceholder') }}" />
<button id="exampleImagesDownloadBtn" class="primary-btn">
<i class="fas fa-download"></i> <span id="exampleDownloadBtnText">{{ t('settings.exampleImages.download') }}</span>
</button>
</div>
</div>
</div>
{{ sm.setting_toggle('autoDownloadExampleImages', 'auto_download_example_images', 'settings.exampleImages.autoDownload', 'settings.exampleImages.autoDownloadHelp') }}
{{ sm.setting_toggle('optimizeExampleImages', 'optimize_example_images', 'settings.exampleImages.optimizeImages', 'settings.exampleImages.optimizeImagesHelp') }}
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="exampleImagesOpenMode">
{{ t('settings.exampleImages.openMode') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.exampleImages.openModeHelp') }}"></i>
<a class="settings-action-link" href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Remote-Open-for-Example-Images" target="_blank" rel="noopener" title="{{ t('settings.exampleImages.openModeWikiLink') }}">
<i class="fas fa-question-circle" aria-hidden="true"></i>
</a>
</label>
</div>
<div class="setting-control select-control">
<select id="exampleImagesOpenMode" onchange="settingsManager.handleExampleImagesOpenModeChange()">
<option value="system">{{ t('settings.exampleImages.openModeOptions.system') }}</option>
<option value="clipboard">{{ t('settings.exampleImages.openModeOptions.clipboard') }}</option>
<option value="uri_template">{{ t('settings.exampleImages.openModeOptions.uriTemplate') }}</option>
</select>
</div>
</div>
</div>
<div class="setting-item" id="exampleImagesLocalRootSetting" style="display: none;">
<div class="setting-row">
<div class="setting-info">
<label for="exampleImagesLocalRoot">
{{ t('settings.exampleImages.localRoot') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.exampleImages.localRootHelp') }}"></i>
</label>
</div>
<div class="setting-control path-control">
<input
type="text"
id="exampleImagesLocalRoot"
placeholder="{{ t('settings.exampleImages.localRootPlaceholder') }}"
onchange="settingsManager.saveInputSetting('exampleImagesLocalRoot', 'example_images_local_root')" />
</div>
</div>
</div>
<div class="setting-item" id="exampleImagesUriTemplateSetting" style="display: none;">
<div class="setting-row">
<div class="setting-info">
<label for="exampleImagesOpenUriTemplate">
{{ t('settings.exampleImages.uriTemplate') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.exampleImages.uriTemplateHelp') }} {{ t('settings.exampleImages.uriTemplatePlaceholders') }}"></i>
</label>
</div>
<div class="setting-control path-control">
<input
type="text"
id="exampleImagesOpenUriTemplate"
placeholder="{{ t('settings.exampleImages.uriTemplatePlaceholder') }}"
onchange="settingsManager.saveInputSetting('exampleImagesOpenUriTemplate', 'example_images_open_uri_template')" />
</div>
</div>
</div>
</div>
<!-- Auto-organize -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.autoOrganize') }}
<!-- Auto-organize Exclusions -->
<div class="setting-item auto-organize-exclusions-item">
<div class="setting-row">
<div class="setting-info">
<label for="autoOrganizeExclusions">
{{ t('settings.autoOrganizeExclusions.label') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.autoOrganizeExclusions.help') }}"></i>
</label>
</div>
</div>
<textarea id="autoOrganizeExclusions" class="priority-tags-input auto-organize-exclusions-input" placeholder="{{ t('settings.autoOrganizeExclusions.placeholder') }}"></textarea>
<div class="settings-input-error-message" id="autoOrganizeExclusionsError"></div>
</div>
</div>
<!-- Metadata -->
<div class="settings-subsection">
{{ sm.subsection_header('settings.sections.metadata') }}
<!-- Metadata Refresh Skip Paths -->
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="metadataRefreshSkipPaths">
{{ t('settings.metadataRefreshSkipPaths.label') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.metadataRefreshSkipPaths.help') }}"></i>
</label>
</div>
</div>
<textarea id="metadataRefreshSkipPaths" class="priority-tags-input auto-organize-exclusions-input" placeholder="{{ t('settings.metadataRefreshSkipPaths.placeholder') }}"></textarea>
<div class="settings-input-error-message" id="metadataRefreshSkipPathsError"></div>
</div>
<!-- CivArchive API provider toggle -->
{{ sm.setting_toggle('enableCivarchiveApi', 'enable_civarchive_api', 'settings.metadataArchive.enableCivarchiveApi', 'settings.metadataArchive.enableCivarchiveApiHelp') }}
<!-- Metadata Archive DB -->
{{ sm.setting_toggle('enableMetadataArchive', 'enable_metadata_archive_db', 'settings.metadataArchive.enableArchiveDb', 'settings.metadataArchive.enableArchiveDbHelp') }}
<div class="setting-item">
<div class="metadata-archive-status" id="metadataArchiveStatus">
<!-- Status will be populated by JavaScript -->
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label>
{{ t('settings.metadataArchive.management') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.metadataArchive.managementHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<button type="button" id="downloadMetadataArchiveBtn" class="primary-btn" onclick="settingsManager.downloadMetadataArchive()">
{{ t('settings.metadataArchive.downloadButton') }}
</button>
<button type="button" id="removeMetadataArchiveBtn" class="danger-btn" onclick="settingsManager.removeMetadataArchive()" style="margin-left: 10px;">
{{ t('settings.metadataArchive.removeButton') }}
</button>
</div>
</div>
</div>
<!-- Metadata provider fallback order -->
{{ sm.setting_select('metadataProviderOrder', 'metadata_provider_order', 'settings.metadataArchive.providerOrder', [
('civitai_archive_sqlite', 'settings.metadataArchive.providerOrderCivitaiArchiveSqlite'),
('civitai_sqlite_archive', 'settings.metadataArchive.providerOrderCivitaiSqliteArchive'),
], 'settings.metadataArchive.providerOrderHelp') }}
</div>
</div>
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -2893,14 +2893,14 @@ class AutoComplete {
summary: command.feedbackSummary || (enabled ? 'Autocomplete Enabled' : 'Autocomplete Disabled'),
detail: command.feedbackDetail || (enabled
? 'Tag autocomplete is now ON. Type to see suggestions.'
: 'Tag autocomplete is now OFF. Use /ac to re-enable.'),
: 'Tag autocomplete is now OFF. Use /autocomplete to re-enable.'),
life: 3000
});
}
/**
* Clear the current command token from input
* Preserves leading spaces after delimiters (e.g., "1girl, /ac" -> "1girl, ")
* Preserves leading spaces after delimiters (e.g., "1girl, /emb" -> "1girl, ")
*/
_clearCurrentToken() {
const currentValue = this.inputElement.value;
@@ -2909,7 +2909,7 @@ class AutoComplete {
const lastSegment = activeRange.rawText;
// Find the command start position, preserving leading spaces
// lastSegment includes leading spaces (e.g., " /ac"), find where command actually starts
// lastSegment includes leading spaces (e.g., " /emb"), find where command actually starts
const commandMatch = lastSegment.match(/^(\s*)(\/\w+)/);
if (commandMatch) {
// commandMatch[1] is leading spaces, commandMatch[2] is the command
+17
View File
@@ -722,6 +722,8 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
menu.className = 'lm-lora-context-menu';
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
// Render off-screen until measured so we can adjust for viewport edges
menu.style.visibility = 'hidden';
// View on Civitai option with globe icon
const viewOnCivitaiOption = createMenuItem(
@@ -978,6 +980,21 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
document.body.appendChild(menu);
// Flip/clamp position so the menu stays fully inside the viewport
const VIEWPORT_MARGIN = 8;
const menuRect = menu.getBoundingClientRect();
let menuLeft = x;
let menuTop = y;
if (menuLeft + menuRect.width > window.innerWidth - VIEWPORT_MARGIN) {
menuLeft = Math.max(VIEWPORT_MARGIN, window.innerWidth - menuRect.width - VIEWPORT_MARGIN);
}
if (menuTop + menuRect.height > window.innerHeight - VIEWPORT_MARGIN) {
menuTop = Math.max(VIEWPORT_MARGIN, window.innerHeight - menuRect.height - VIEWPORT_MARGIN);
}
menu.style.left = `${menuLeft}px`;
menu.style.top = `${menuTop}px`;
menu.style.visibility = '';
// Close menu when clicking outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
+1 -1
View File
@@ -430,7 +430,7 @@ app.registerExtension({
name: "Search LoRA autocomplete within active filters",
type: "boolean",
defaultValue: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT,
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. Commands /af and /noaf toggle this mode.",
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. Commands /activefilters and /noactivefilters toggle this mode.",
category: ["LoRA Manager", "Autocomplete", "LoRA Active Filters"],
},
{