feat(localization): enhance import modal and related components with new labels, descriptions, and error messages for improved user experience

This commit is contained in:
Will Miao
2025-08-31 22:41:35 +08:00
parent 4956d6781f
commit 92341111ad
8 changed files with 206 additions and 93 deletions

View File

@@ -290,7 +290,59 @@
"controls": {
"import": {
"action": "Import Recipe",
"title": "Import a recipe from image or URL"
"title": "Import a recipe from image or URL",
"urlLocalPath": "URL / Local Path",
"uploadImage": "Upload Image",
"urlSectionDescription": "Input a Civitai image URL or local file path to import as a recipe.",
"imageUrlOrPath": "Image URL or File Path:",
"urlPlaceholder": "https://civitai.com/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)",
"addTagPlaceholder": "Add a tag",
"addTag": "Add",
"noTagsAdded": "No tags added",
"lorasInRecipe": "LoRAs in this Recipe",
"downloadLocationPreview": "Download Location Preview:",
"useDefaultPath": "Use Default Path",
"useDefaultPathTooltip": "When enabled, files are automatically organized using configured path templates",
"selectLoraRoot": "Select a LoRA root directory",
"targetFolderPath": "Target Folder Path:",
"folderPathPlaceholder": "Type folder path or select from tree below...",
"createNewFolder": "Create new folder",
"root": "Root",
"browseFolders": "Browse Folders:",
"downloadAndSaveRecipe": "Download & Save Recipe",
"downloadMissingLoras": "Download Missing LoRAs",
"saveRecipe": "Save Recipe",
"loraCountInfo": "({existing}/{total} in library)",
"processingInput": "Processing input...",
"analyzingMetadata": "Analyzing image metadata...",
"downloadingLoras": "Downloading LoRAs...",
"savingRecipe": "Saving recipe...",
"startingDownload": "Starting download for LoRA {current}/{total}",
"deletedFromCivitai": "Deleted from Civitai",
"inLibrary": "In Library",
"notInLibrary": "Not in Library",
"earlyAccessRequired": "This LoRA requires early access payment to download.",
"earlyAccessEnds": "Early access ends on {date}.",
"earlyAccess": "Early Access",
"verifyEarlyAccess": "Verify that you have purchased early access before downloading.",
"duplicateRecipesFound": "{count} identical recipe(s) found in your library",
"duplicateRecipesDescription": "These recipes contain the same LoRAs with identical weights.",
"showDuplicates": "Show duplicates",
"hideDuplicates": "Hide duplicates",
"loraCount": "{count} LoRAs",
"recipePreviewAlt": "Recipe preview",
"loraPreviewAlt": "LoRA preview",
"errors": {
"selectImageFile": "Please select an image file",
"enterUrlOrPath": "Please enter a URL or file path",
"selectLoraRoot": "Please select a LoRA root directory"
}
},
"refresh": {
"title": "Refresh recipe list"
@@ -832,7 +884,15 @@
"preparingForSharing": "Preparing recipe for sharing...",
"downloadStarted": "Recipe download started",
"shareError": "Error sharing recipe: {message}",
"sharePreparationError": "Error preparing recipe for sharing"
"sharePreparationError": "Error preparing recipe for sharing",
"selectImageFirst": "Please select an image first",
"enterRecipeName": "Please enter a recipe name",
"processingError": "Processing error: {message}",
"folderBrowserError": "Error loading folder browser: {message}",
"recipeSaveFailed": "Failed to save recipe: {error}",
"importFailed": "Import failed: {message}",
"folderTreeFailed": "Failed to load folder tree",
"folderTreeError": "Error loading folder tree"
},
"models": {
"noModelsSelected": "No models selected",
@@ -890,15 +950,10 @@
"downloadError": "Download error: {message}"
},
"import": {
"enterRecipeName": "Please enter a recipe name",
"selectImageFirst": "Please select an image first",
"folderTreeFailed": "Failed to load folder tree",
"folderTreeError": "Error loading folder tree",
"imagesImported": "Example images imported successfully",
"importFailed": "Failed to import example images: {message}",
"recipeSaveFailed": "Failed to save recipe: {error}",
"processingError": "Processing error: {message}",
"folderBrowserError": "Folder browser error: {message}"
"importFailed": "Failed to import example images: {message}"
},
"triggerWords": {
"loadFailed": "Could not load trained words",

View File

@@ -290,7 +290,59 @@
"controls": {
"import": {
"action": "导入配方",
"title": "从图片或 URL 导入配方"
"title": "从图片或 URL 导入配方",
"urlLocalPath": "URL / 本地路径",
"uploadImage": "上传图片",
"urlSectionDescription": "输入 Civitai 图片 URL 或本地文件路径以导入为配方。",
"imageUrlOrPath": "图片 URL 或文件路径:",
"urlPlaceholder": "https://civitai.com/images/... 或 C:/path/to/image.png",
"fetchImage": "获取图片",
"uploadSectionDescription": "上传带有 LoRA 元数据的图片以导入为配方。",
"selectImage": "选择图片",
"recipeName": "配方名称",
"recipeNamePlaceholder": "输入配方名称",
"tagsOptional": "标签(可选)",
"addTagPlaceholder": "添加标签",
"addTag": "添加",
"noTagsAdded": "未添加标签",
"lorasInRecipe": "此配方中的 LoRA",
"downloadLocationPreview": "下载位置预览:{path}",
"useDefaultPath": "使用默认路径",
"useDefaultPathTooltip": "启用后,文件将自动使用配置的路径模板进行组织",
"selectLoraRoot": "选择 LoRA 根目录",
"targetFolderPath": "目标文件夹路径:",
"folderPathPlaceholder": "输入文件夹路径或从下面的树中选择...",
"createNewFolder": "创建新文件夹",
"root": "根目录",
"browseFolders": "浏览文件夹:",
"downloadAndSaveRecipe": "下载并保存配方",
"downloadMissingLoras": "下载缺失的 LoRA",
"saveRecipe": "保存配方",
"loraCountInfo": "({existing}/{total} in library)",
"processingInput": "处理输入...",
"analyzingMetadata": "分析图像元数据...",
"downloadingLoras": "下载 LoRA...",
"savingRecipe": "保存配方...",
"startingDownload": "开始下载 LoRA {current}/{total}",
"deletedFromCivitai": "从 Civitai 中删除",
"inLibrary": "在库中",
"notInLibrary": "不在库中",
"earlyAccessRequired": "此 LoRA 需要提前访问权限才能下载。",
"earlyAccessEnds": "提前访问权限将于 {date} 结束。",
"earlyAccess": "提前访问",
"verifyEarlyAccess": "在下载之前,请验证您是否已购买提前访问权限。",
"duplicateRecipesFound": "在您的库中找到 {count} 个相同的配方。",
"duplicateRecipesDescription": "这些配方包含相同的 LoRA权重完全相同。",
"showDuplicates": "显示重复项",
"hideDuplicates": "隐藏重复项",
"loraCount": "{count} LoRA",
"recipePreviewAlt": "配方预览",
"loraPreviewAlt": "LoRA 预览",
"errors": {
"selectImageFile": "请选择一个图像文件",
"enterUrlOrPath": "请输入 URL 或文件路径",
"selectLoraRoot": "请选择 LoRA 根目录"
}
},
"refresh": {
"title": "刷新配方列表"
@@ -832,7 +884,15 @@
"preparingForSharing": "正在准备分享配方...",
"downloadStarted": "配方下载已开始",
"shareError": "分享配方出错:{message}",
"sharePreparationError": "准备分享配方出错"
"sharePreparationError": "准备分享配方出错",
"selectImageFirst": "请先选择图片",
"enterRecipeName": "请输入配方名称",
"processingError": "处理出错:{message}",
"folderBrowserError": "加载文件夹浏览器出错:{message}",
"recipeSaveFailed": "保存配方失败:{error}",
"importFailed": "导入失败:{message}",
"folderTreeFailed": "加载文件夹树失败",
"folderTreeError": "加载文件夹树出错"
},
"models": {
"noModelsSelected": "未选中模型",
@@ -890,15 +950,10 @@
"downloadError": "下载错误:{message}"
},
"import": {
"enterRecipeName": "请输入配方名称",
"selectImageFirst": "请先选择图片",
"folderTreeFailed": "加载文件夹树失败",
"folderTreeError": "加载文件夹树出错",
"imagesImported": "示例图片导入成功",
"importFailed": "导入示例图片失败:{message}",
"recipeSaveFailed": "保存配方失败:{error}",
"processingError": "处理出错:{message}",
"folderBrowserError": "文件夹浏览器出错:{message}"
"importFailed": "导入示例图片失败:{message}"
},
"triggerWords": {
"loadFailed": "无法加载训练词",

View File

@@ -11,6 +11,7 @@ import { getModelApiClient } from '../api/modelApiFactory.js';
import { state } from '../state/index.js';
import { MODEL_TYPES } from '../api/apiConfig.js';
import { showToast } from '../utils/uiHelpers.js';
import { translate } from '../utils/i18nHelpers.js';
export class ImportManager {
constructor() {
@@ -110,7 +111,7 @@ export class ImportManager {
if (recipeName) recipeName.value = '';
const tagsContainer = document.getElementById('tagsContainer');
if (tagsContainer) tagsContainer.innerHTML = '<div class="empty-tags">No tags added</div>';
if (tagsContainer) tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
// Clear folder path input
const folderPathInput = document.getElementById('importFolderPath');
@@ -261,7 +262,7 @@ export class ImportManager {
this.updateTargetPath();
} catch (error) {
showToast('toast.import.importFailed', { message: error.message }, 'error');
showToast('toast.recipes.importFailed', { message: error.message }, 'error');
}
}
@@ -350,11 +351,11 @@ export class ImportManager {
await this.folderTreeManager.loadTree(treeData.tree);
} else {
console.error('Failed to fetch folder tree:', treeData.error);
showToast('toast.import.folderTreeFailed', {}, 'error');
showToast('toast.recipes.folderTreeFailed', {}, 'error');
}
} catch (error) {
console.error('Error initializing folder tree:', error);
showToast('toast.import.folderTreeError', {}, 'error');
showToast('toast.recipes.folderTreeError', {}, 'error');
}
}
@@ -368,9 +369,7 @@ export class ImportManager {
const pathDisplay = document.getElementById('importTargetPathDisplay');
const loraRoot = document.getElementById('importLoraRoot').value;
let fullPath = loraRoot || 'Select a LoRA root directory';
if (loraRoot) {
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) {
if (this.useDefaultPath) {
// Show actual template path
try {
@@ -425,11 +424,11 @@ export class ImportManager {
// Update the modal title
const modalTitle = document.querySelector('#importModal h2');
if (modalTitle) modalTitle.textContent = 'Download Missing LoRAs';
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Update the save button text
const saveButton = document.querySelector('#locationStep .primary-btn');
if (saveButton) saveButton.textContent = 'Download Missing LoRAs';
if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Hide the back button
const backButton = document.querySelector('#locationStep .secondary-btn');

View File

@@ -1,4 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { MODEL_TYPES } from '../../api/apiConfig.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
@@ -13,13 +14,13 @@ export class DownloadManager {
const isDownloadOnly = !!this.importManager.recipeId;
if (!isDownloadOnly && !this.importManager.recipeName) {
showToast('toast.import.enterRecipeName', {}, 'error');
showToast('toast.recipes.enterRecipeName', {}, 'error');
return;
}
try {
// Show progress indicator
this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? 'Downloading LoRAs...' : 'Saving recipe...');
this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...'));
// Only send the complete recipe to save if not in download-only mode
if (!isDownloadOnly) {
@@ -77,7 +78,7 @@ export class DownloadManager {
if (!result.success) {
// Handle save error
console.error("Failed to save recipe:", result.error);
showToast('toast.import.recipeSaveFailed', { error: result.error }, 'error');
showToast('toast.recipes.recipeSaveFailed', { error: result.error }, 'error');
// Close modal
modalManager.closeModal('importModal');
return;
@@ -107,7 +108,7 @@ export class DownloadManager {
} catch (error) {
console.error('Error:', error);
showToast('toast.import.processingError', { message: error.message }, 'error');
showToast('toast.recipes.processingError', { message: error.message }, 'error');
} finally {
this.importManager.loadingManager.hide();
}
@@ -117,7 +118,7 @@ export class DownloadManager {
// For download, we need to validate the target path
const loraRoot = document.getElementById('importLoraRoot')?.value;
if (!loraRoot) {
throw new Error('Please select a LoRA root directory');
throw new Error(translate('recipes.controls.import.errors.selectLoraRoot', {}, 'Please select a LoRA root directory'));
}
// Build target path
@@ -195,7 +196,7 @@ export class DownloadManager {
currentLoraProgress = 0;
// Initial status update for new LoRA
this.importManager.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.importManager.downloadableLoRAs.length}`);
this.importManager.loadingManager.setStatus(translate('recipes.controls.import.startingDownload', { current: i+1, total: this.importManager.downloadableLoRAs.length }, `Starting download for LoRA ${i+1}/${this.importManager.downloadableLoRAs.length}`));
updateProgress(0, completedDownloads, lora.name);
try {

View File

@@ -1,4 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
export class FolderBrowser {
@@ -136,7 +137,7 @@ export class FolderBrowser {
this.initializeFolderBrowser();
} catch (error) {
console.error('Error in API calls:', error);
showToast('toast.import.folderBrowserError', { message: error.message }, 'error');
showToast('toast.recipes.folderBrowserError', { message: error.message }, 'error');
}
}
@@ -204,7 +205,7 @@ export class FolderBrowser {
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
const newFolder = document.getElementById('importNewFolder')?.value?.trim() || '';
let fullPath = loraRoot || 'Select a LoRA root directory';
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory');
if (loraRoot) {
if (this.importManager.selectedFolder) {

View File

@@ -1,4 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
export class ImageProcessor {
constructor(importManager) {
@@ -13,7 +14,7 @@ export class ImageProcessor {
// Validate file type
if (!file.type.match('image.*')) {
errorElement.textContent = 'Please select an image file';
errorElement.textContent = translate('recipes.controls.import.errors.selectImageFile', {}, 'Please select an image file');
return;
}
@@ -32,7 +33,7 @@ export class ImageProcessor {
// Validate input
if (!input) {
errorElement.textContent = 'Please enter a URL or file path';
errorElement.textContent = translate('recipes.controls.import.errors.enterUrlOrPath', {}, 'Please enter a URL or file path');
return;
}
@@ -40,7 +41,7 @@ export class ImageProcessor {
errorElement.textContent = '';
// Show loading indicator
this.importManager.loadingManager.showSimpleLoading('Processing input...');
this.importManager.loadingManager.showSimpleLoading(translate('recipes.controls.import.processingInput', {}, 'Processing input...'));
try {
// Check if it's a URL or a local file path
@@ -156,12 +157,12 @@ export class ImageProcessor {
async uploadAndAnalyzeImage() {
if (!this.importManager.recipeImage) {
showToast('toast.import.selectImageFirst', {}, 'error');
showToast('toast.recipes.selectImageFirst', {}, 'error');
return;
}
try {
this.importManager.loadingManager.showSimpleLoading('Analyzing image metadata...');
this.importManager.loadingManager.showSimpleLoading(translate('recipes.controls.import.analyzingMetadata', {}, 'Analyzing image metadata...'));
// Create form data for upload
const formData = new FormData();

View File

@@ -1,4 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
export class RecipeDataManager {
constructor(importManager) {
@@ -62,17 +63,17 @@ export class RecipeDataManager {
// For file upload mode
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.innerHTML = `<img src="${e.target.result}" alt="Recipe preview">`;
imagePreview.innerHTML = `<img src="${e.target.result}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}">`;
};
reader.readAsDataURL(this.importManager.recipeImage);
} else if (this.importManager.recipeData && this.importManager.recipeData.image_base64) {
// For URL mode - use the base64 image data returned from the backend
imagePreview.innerHTML = `<img src="data:image/jpeg;base64,${this.importManager.recipeData.image_base64}" alt="Recipe preview">`;
imagePreview.innerHTML = `<img src="data:image/jpeg;base64,${this.importManager.recipeData.image_base64}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}">`;
} else if (this.importManager.importMode === 'url') {
// Fallback for URL mode if no base64 data
const urlInput = document.getElementById('imageUrlInput');
if (urlInput && urlInput.value) {
imagePreview.innerHTML = `<img src="${urlInput.value}" alt="Recipe preview" crossorigin="anonymous">`;
imagePreview.innerHTML = `<img src="${urlInput.value}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}" crossorigin="anonymous">`;
}
}
}
@@ -82,7 +83,7 @@ export class RecipeDataManager {
const existingLoras = this.importManager.recipeData.loras.filter(lora => lora.existsLocally).length;
const loraCountInfo = document.getElementById('loraCountInfo');
if (loraCountInfo) {
loraCountInfo.textContent = `(${existingLoras}/${totalLoras} in library)`;
loraCountInfo.textContent = translate('recipes.controls.import.loraCountInfo', { existing: existingLoras, total: totalLoras }, `(${existingLoras}/${totalLoras} in library)`);
}
// Display LoRAs list
@@ -98,16 +99,16 @@ export class RecipeDataManager {
let statusBadge;
if (isDeleted) {
statusBadge = `<div class="deleted-badge">
<i class="fas fa-exclamation-circle"></i> Deleted from Civitai
<i class="fas fa-exclamation-circle"></i> ${translate('recipes.controls.import.deletedFromCivitai', {}, 'Deleted from Civitai')}
</div>`;
} else {
statusBadge = existsLocally ?
`<div class="local-badge">
<i class="fas fa-check"></i> In Library
<i class="fas fa-check"></i> ${translate('recipes.controls.import.inLibrary', {}, 'In Library')}
<div class="local-path">${localPath}</div>
</div>` :
`<div class="missing-badge">
<i class="fas fa-exclamation-triangle"></i> Not in Library
<i class="fas fa-exclamation-triangle"></i> ${translate('recipes.controls.import.notInLibrary', {}, 'Not in Library')}
</div>`;
}
@@ -115,20 +116,20 @@ export class RecipeDataManager {
let earlyAccessBadge = '';
if (isEarlyAccess) {
// Format the early access end date if available
let earlyAccessInfo = 'This LoRA requires early access payment to download.';
let earlyAccessInfo = translate('recipes.controls.import.earlyAccessRequired', {}, 'This LoRA requires early access payment to download.');
if (lora.earlyAccessEndsAt) {
try {
const endDate = new Date(lora.earlyAccessEndsAt);
const formattedDate = endDate.toLocaleDateString();
earlyAccessInfo += ` Early access ends on ${formattedDate}.`;
earlyAccessInfo += ` ${translate('recipes.controls.import.earlyAccessEnds', { date: formattedDate }, `Early access ends on ${formattedDate}.`)}`;
} catch (e) {
console.warn('Failed to format early access date', e);
}
}
earlyAccessBadge = `<div class="early-access-badge">
<i class="fas fa-clock"></i> Early Access
<div class="early-access-info">${earlyAccessInfo} Verify that you have purchased early access before downloading.</div>
<i class="fas fa-clock"></i> ${translate('recipes.controls.import.earlyAccess', {}, 'Early Access')}
<div class="early-access-info">${earlyAccessInfo} ${translate('recipes.controls.import.verifyEarlyAccess', {}, 'Verify that you have purchased early access before downloading.')}</div>
</div>`;
}
@@ -139,7 +140,7 @@ export class RecipeDataManager {
return `
<div class="lora-item ${existsLocally ? 'exists-locally' : isDeleted ? 'is-deleted' : 'missing-locally'} ${isEarlyAccess ? 'is-early-access' : ''}">
<div class="lora-thumbnail">
<img src="${lora.thumbnailUrl || '/loras_static/images/no-preview.png'}" alt="LoRA preview">
<img src="${lora.thumbnailUrl || '/loras_static/images/no-preview.png'}" alt="${translate('recipes.controls.import.loraPreviewAlt', {}, 'LoRA preview')}">
</div>
<div class="lora-content">
<div class="lora-header">
@@ -232,12 +233,12 @@ export class RecipeDataManager {
<div class="warning-icon"><i class="fas fa-clone"></i></div>
<div class="warning-content">
<div class="warning-title">
${this.importManager.duplicateRecipes.length} identical ${this.importManager.duplicateRecipes.length === 1 ? 'recipe' : 'recipes'} found in your library
${translate('recipes.controls.import.duplicateRecipesFound', { count: this.importManager.duplicateRecipes.length }, `${this.importManager.duplicateRecipes.length} identical ${this.importManager.duplicateRecipes.length === 1 ? 'recipe' : 'recipes'} found in your library`)}
</div>
<div class="warning-text">
These recipes contain the same LoRAs with identical weights.
${translate('recipes.controls.import.duplicateRecipesDescription', {}, 'These recipes contain the same LoRAs with identical weights.')}
<button id="toggleDuplicatesList" class="toggle-duplicates-btn">
Show duplicates <i class="fas fa-chevron-down"></i>
${translate('recipes.controls.import.showDuplicates', {}, 'Show duplicates')} <i class="fas fa-chevron-down"></i>
</button>
</div>
</div>
@@ -246,7 +247,7 @@ export class RecipeDataManager {
${this.importManager.duplicateRecipes.map((recipe) => `
<div class="duplicate-recipe-card">
<div class="duplicate-recipe-preview">
<img src="${recipe.file_url}" alt="Recipe preview">
<img src="${recipe.file_url}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}">
<div class="duplicate-recipe-title">${recipe.title}</div>
</div>
<div class="duplicate-recipe-details">
@@ -254,7 +255,7 @@ export class RecipeDataManager {
<i class="fas fa-calendar-alt"></i> ${formatDate(recipe.modified)}
</div>
<div class="duplicate-recipe-lora-count">
<i class="fas fa-layer-group"></i> ${recipe.lora_count} LoRAs
<i class="fas fa-layer-group"></i> ${translate('recipes.controls.import.loraCount', { count: recipe.lora_count }, `${recipe.lora_count} LoRAs`)}
</div>
</div>
</div>
@@ -275,9 +276,9 @@ export class RecipeDataManager {
const icon = toggleButton.querySelector('i');
if (icon) {
if (list.classList.contains('collapsed')) {
toggleButton.innerHTML = `Show duplicates <i class="fas fa-chevron-down"></i>`;
toggleButton.innerHTML = `${translate('recipes.controls.import.showDuplicates', {}, 'Show duplicates')} <i class="fas fa-chevron-down"></i>`;
} else {
toggleButton.innerHTML = `Hide duplicates <i class="fas fa-chevron-up"></i>`;
toggleButton.innerHTML = `${translate('recipes.controls.import.hideDuplicates', {}, 'Hide duplicates')} <i class="fas fa-chevron-up"></i>`;
}
}
}
@@ -362,9 +363,9 @@ export class RecipeDataManager {
nextButton.classList.remove('warning-btn');
if (missingNotDeleted > 0) {
nextButton.textContent = 'Download Missing LoRAs';
nextButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
} else {
nextButton.textContent = 'Save Recipe';
nextButton.textContent = translate('recipes.controls.import.saveRecipe', {}, 'Save Recipe');
}
}
@@ -391,7 +392,7 @@ export class RecipeDataManager {
const tagsContainer = document.getElementById('tagsContainer');
if (this.importManager.recipeTags.length === 0) {
tagsContainer.innerHTML = '<div class="empty-tags">No tags added</div>';
tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
return;
}
@@ -406,7 +407,7 @@ export class RecipeDataManager {
proceedFromDetails() {
// Validate recipe name
if (!this.importManager.recipeName) {
showToast('toast.import.enterRecipeName', {}, 'error');
showToast('toast.recipes.enterRecipeName', {}, 'error');
return;
}

View File

@@ -2,29 +2,29 @@
<div class="modal-content">
<div class="modal-header">
<button class="close" onclick="modalManager.closeModal('importModal')">&times;</button>
<h2>Import Recipe</h2>
<h2>{{ t('recipes.controls.import.action') }}</h2>
</div>
<!-- Step 1: Upload Image or Input URL -->
<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> URL / Local Path
<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> Upload Image
<i class="fas fa-upload"></i> {{ t('recipes.controls.import.uploadImage') }}
</button>
</div>
<!-- Input URL/Path Section -->
<div class="import-section" id="urlSection">
<p>Input a Civitai image URL or local file path to import as a recipe.</p>
<p>{{ t('recipes.controls.import.urlSectionDescription') }}</p>
<div class="input-group">
<label for="imageUrlInput">Image URL or File Path:</label>
<label for="imageUrlInput">{{ t('recipes.controls.import.imageUrlOrPath') }}</label>
<div class="input-with-button">
<input type="text" id="imageUrlInput" placeholder="https://civitai.com/images/... or C:/path/to/image.png">
<input type="text" id="imageUrlInput" placeholder="{{ t('recipes.controls.import.urlPlaceholder') }}">
<button class="primary-btn" onclick="importManager.handleUrlInput()">
<i class="fas fa-download"></i> Fetch Image
<i class="fas fa-download"></i> {{ t('recipes.controls.import.fetchImage') }}
</button>
</div>
<div class="error-message" id="importUrlError"></div>
@@ -33,13 +33,13 @@
<!-- Upload Image Section -->
<div class="import-section" id="uploadSection">
<p>Upload an image with LoRA metadata to import as a recipe.</p>
<p>{{ t('recipes.controls.import.uploadSectionDescription') }}</p>
<div class="input-group">
<label for="recipeImageUpload">Select Image:</label>
<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> Select Image
<i class="fas fa-upload"></i> {{ t('recipes.controls.import.selectImage') }}
</div>
</div>
<div class="error-message" id="uploadError"></div>
@@ -47,7 +47,7 @@
</div>
<div class="modal-actions">
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">Cancel</button>
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">{{ t('common.actions.cancel') }}</button>
</div>
</div>
@@ -60,28 +60,28 @@
<div class="recipe-form-container">
<div class="input-group">
<label for="recipeName">Recipe Name</label>
<input type="text" id="recipeName" placeholder="Enter recipe name"
<label for="recipeName">{{ t('recipes.controls.import.recipeName') }}</label>
<input type="text" id="recipeName" placeholder="{{ t('recipes.controls.import.recipeNamePlaceholder') }}"
onchange="importManager.handleRecipeNameChange(event)">
</div>
<div class="input-group">
<label>Tags (optional)</label>
<label>{{ t('recipes.controls.import.tagsOptional') }}</label>
<div class="tag-input-container">
<input type="text" id="tagInput" placeholder="Add a tag">
<input type="text" id="tagInput" placeholder="{{ t('recipes.controls.import.addTagPlaceholder') }}">
<button class="secondary-btn" onclick="importManager.addTag()">
<i class="fas fa-plus"></i> Add
<i class="fas fa-plus"></i> {{ t('recipes.controls.import.addTag') }}
</button>
</div>
<div id="tagsContainer" class="tags-container">
<div class="empty-tags">No tags added</div>
<div class="empty-tags">{{ t('recipes.controls.import.noTagsAdded') }}</div>
</div>
</div>
</div>
</div>
<div class="input-group">
<label>LoRAs in this Recipe <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
<label>{{ t('recipes.controls.import.lorasInRecipe') }} <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
<div id="lorasList" class="loras-list">
<!-- LoRAs will be populated here -->
</div>
@@ -93,8 +93,8 @@
</div>
<div class="modal-actions">
<button class="secondary-btn" onclick="importManager.backToUpload()">Back</button>
<button class="primary-btn" onclick="importManager.proceedFromDetails()">Next</button>
<button class="secondary-btn" onclick="importManager.backToUpload()">{{ t('common.actions.back') }}</button>
<button class="primary-btn" onclick="importManager.proceedFromDetails()">{{ t('common.actions.next') }}</button>
</div>
</div>
@@ -104,9 +104,9 @@
<!-- Path preview with inline toggle -->
<div class="path-preview">
<div class="path-preview-header">
<label>Download Location Preview:</label>
<div class="inline-toggle-container" title="When enabled, files are automatically organized using configured path templates">
<span class="inline-toggle-label">Use Default Path</span>
<label>{{ t('recipes.controls.import.downloadLocationPreview') }}</label>
<div class="inline-toggle-container" title="{{ t('recipes.controls.import.useDefaultPathTooltip') }}">
<span class="inline-toggle-label">{{ t('recipes.controls.import.useDefaultPath') }}</span>
<div class="toggle-switch">
<input type="checkbox" id="importUseDefaultPath">
<label for="importUseDefaultPath" class="toggle-slider"></label>
@@ -114,13 +114,13 @@
</div>
</div>
<div class="path-display" id="importTargetPathDisplay">
<span class="path-text">Select a LoRA root directory</span>
<span class="path-text">{{ t('recipes.controls.import.selectLoraRoot') }}</span>
</div>
</div>
<!-- Model Root Selection -->
<div class="input-group">
<label for="importLoraRoot">Select LoRA Root:</label>
<label for="importLoraRoot">{{ t('recipes.controls.import.selectLoraRoot') }}</label>
<select id="importLoraRoot"></select>
</div>
@@ -128,10 +128,10 @@
<div class="manual-path-selection" id="importManualPathSelection">
<!-- Path input with autocomplete -->
<div class="input-group">
<label for="importFolderPath">Target Folder Path:</label>
<label for="importFolderPath">{{ t('recipes.controls.import.targetFolderPath') }}</label>
<div class="path-input-container">
<input type="text" id="importFolderPath" placeholder="Type folder path or select from tree below..." autocomplete="off" />
<button type="button" id="importCreateFolderBtn" class="create-folder-btn" title="Create new folder">
<input type="text" id="importFolderPath" placeholder="{{ t('recipes.controls.import.folderPathPlaceholder') }}" autocomplete="off" />
<button type="button" id="importCreateFolderBtn" class="create-folder-btn" title="{{ t('recipes.controls.import.createNewFolder') }}">
<i class="fas fa-plus"></i>
</button>
</div>
@@ -141,13 +141,13 @@
<!-- Breadcrumb navigation -->
<div class="breadcrumb-nav" id="importBreadcrumbNav">
<span class="breadcrumb-item root" data-path="">
<i class="fas fa-home"></i> Root
<i class="fas fa-home"></i> {{ t('recipes.controls.import.root') }}
</span>
</div>
<!-- Hierarchical folder tree -->
<div class="input-group">
<label>Browse Folders:</label>
<label>{{ t('recipes.controls.import.browseFolders') }}</label>
<div class="folder-tree-container">
<div class="folder-tree" id="importFolderTree">
<!-- Tree will be loaded dynamically -->
@@ -158,8 +158,8 @@
</div>
<div class="modal-actions">
<button class="secondary-btn" onclick="importManager.backToDetails()">Back</button>
<button class="primary-btn" onclick="importManager.saveRecipe()">Download & Save Recipe</button>
<button class="secondary-btn" onclick="importManager.backToDetails()">{{ t('common.actions.back') }}</button>
<button class="primary-btn" onclick="importManager.saveRecipe()">{{ t('recipes.controls.import.downloadAndSaveRecipe') }}</button>
</div>
</div>
</div>