mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-26 07:35:44 -03:00
feat: Add bulk download missing LoRAs feature for recipes
- Add BulkMissingLoraDownloadManager.js for handling bulk LoRA downloads - Add context menu item to bulk mode for downloading missing LoRAs - Add confirmation modal with deduplicated LoRA list preview - Implement sequential downloading with WebSocket progress updates - Fix CSS class naming conflicts to avoid import-modal.css collision - Update translations for 9 languages (en, zh-CN, zh-TW, ja, ko, ru, de, fr, es, he) - Style modal without internal scrolling for better UX
This commit is contained in:
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen",
|
||||
"resumeMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle fortsetzen",
|
||||
"deleteAll": "Alle Modelle löschen",
|
||||
"downloadMissingLoras": "Fehlende LoRAs herunterladen",
|
||||
"clear": "Auswahl löschen",
|
||||
"skipMetadataRefreshCount": "Überspringen({count} Modelle)",
|
||||
"resumeMetadataRefreshCount": "Fortsetzen({count} Modelle)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "Basis-Modell aktualisieren",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "Fehlende LoRAs herunterladen",
|
||||
"message": "{uniqueCount} einzigartige fehlende LoRAs gefunden (von insgesamt {totalCount} in ausgewählten Rezepten).",
|
||||
"previewTitle": "Zu herunterladende LoRAs:",
|
||||
"moreItems": "...und {count} weitere",
|
||||
"note": "Dateien werden mit Standard-Pfad-Vorlagen heruntergeladen. Dies kann je nach Anzahl der LoRAs eine Weile dauern.",
|
||||
"downloadButton": "{count} LoRA(s) herunterladen"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "Lokale Beispielbilder",
|
||||
"message": "Keine lokalen Beispielbilder für dieses Modell gefunden. Ansichtsoptionen:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "Keine Rezepte ausgewählt",
|
||||
"noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden",
|
||||
"noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "Keine Modelle ausgewählt",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "Skip Metadata Refresh for Selected",
|
||||
"resumeMetadataRefresh": "Resume Metadata Refresh for Selected",
|
||||
"deleteAll": "Delete Selected Models",
|
||||
"downloadMissingLoras": "Download Missing LoRAs",
|
||||
"clear": "Clear Selection",
|
||||
"skipMetadataRefreshCount": "Skip ({count} models)",
|
||||
"resumeMetadataRefreshCount": "Resume ({count} models)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "Update Base Model",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "Download Missing LoRAs",
|
||||
"message": "Found {uniqueCount} unique missing LoRAs (from {totalCount} total across selected recipes).",
|
||||
"previewTitle": "LoRAs to download:",
|
||||
"moreItems": "...and {count} more",
|
||||
"note": "Files will be downloaded using default path templates. This may take a while depending on the number of LoRAs.",
|
||||
"downloadButton": "Download {count} LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "Local Example Images",
|
||||
"message": "No local example images found for this model. View options:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "No recipes selected",
|
||||
"noMissingLorasInSelection": "No missing LoRAs found in selected recipes",
|
||||
"noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "No models selected",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados",
|
||||
"resumeMetadataRefresh": "Reanudar actualización de metadatos para seleccionados",
|
||||
"deleteAll": "Eliminar todos los modelos",
|
||||
"downloadMissingLoras": "Descargar LoRAs faltantes",
|
||||
"clear": "Limpiar selección",
|
||||
"skipMetadataRefreshCount": "Omitir({count} modelos)",
|
||||
"resumeMetadataRefreshCount": "Reanudar({count} modelos)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "Actualizar modelo base",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "Descargar LoRAs faltantes",
|
||||
"message": "Se encontraron {uniqueCount} LoRAs faltantes únicos (de {totalCount} en total entre las recetas seleccionadas).",
|
||||
"previewTitle": "LoRAs para descargar:",
|
||||
"moreItems": "...y {count} más",
|
||||
"note": "Los archivos se descargarán usando las plantillas de ruta predeterminadas. Esto puede tomar un tiempo dependiendo del número de LoRAs.",
|
||||
"downloadButton": "Descargar {count} LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "Imágenes de ejemplo locales",
|
||||
"message": "No se encontraron imágenes de ejemplo locales para este modelo. Opciones de visualización:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "No se han seleccionado recetas",
|
||||
"noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas",
|
||||
"noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "No hay modelos seleccionados",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "Ignorer l'actualisation des métadonnées pour la sélection",
|
||||
"resumeMetadataRefresh": "Reprendre l'actualisation des métadonnées pour la sélection",
|
||||
"deleteAll": "Supprimer tous les modèles",
|
||||
"downloadMissingLoras": "Télécharger les LoRAs manquants",
|
||||
"clear": "Effacer la sélection",
|
||||
"skipMetadataRefreshCount": "Ignorer({count} modèles)",
|
||||
"resumeMetadataRefreshCount": "Reprendre({count} modèles)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "Mettre à jour le modèle de base",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "Télécharger les LoRAs manquants",
|
||||
"message": "{uniqueCount} LoRAs manquants uniques trouvés (sur un total de {totalCount} dans les recettes sélectionnées).",
|
||||
"previewTitle": "LoRAs à télécharger :",
|
||||
"moreItems": "...et {count} de plus",
|
||||
"note": "Les fichiers seront téléchargés en utilisant les modèles de chemins par défaut. Cela peut prendre un certain temps selon le nombre de LoRAs.",
|
||||
"downloadButton": "Télécharger {count} LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "Images d'exemple locales",
|
||||
"message": "Aucune image d'exemple locale trouvée pour ce modèle. Options d'affichage :",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "Aucune recette sélectionnée",
|
||||
"noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées",
|
||||
"noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "Aucun modèle sélectionné",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים",
|
||||
"resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים",
|
||||
"deleteAll": "מחק את כל המודלים",
|
||||
"downloadMissingLoras": "הורדת LoRAs חסרים",
|
||||
"clear": "נקה בחירה",
|
||||
"skipMetadataRefreshCount": "דילוג({count} מודלים)",
|
||||
"resumeMetadataRefreshCount": "המשך({count} מודלים)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "עדכן מודל בסיס",
|
||||
"cancel": "ביטול"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "הורדת LoRAs חסרים",
|
||||
"message": "נמצאו {uniqueCount} LoRAs חסרים ייחודיים (מתוך {totalCount} בסך הכל במתכונים שנבחרו).",
|
||||
"previewTitle": "LoRAs להורדה:",
|
||||
"moreItems": "...ועוד {count}",
|
||||
"note": "הקבצים יורדו באמצעות תבניות נתיב ברירת מחדל. זה עשוי לקחת זמן בהתאם למספר ה-LoRAs.",
|
||||
"downloadButton": "הורד {count} LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "תמונות דוגמה מקומיות",
|
||||
"message": "לא נמצאו תמונות דוגמה מקומיות למודל זה. אפשרויות צפייה:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "לא נבחרו מתכונים",
|
||||
"noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו",
|
||||
"noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "לא נבחרו מודלים",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ",
|
||||
"resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開",
|
||||
"deleteAll": "すべてのモデルを削除",
|
||||
"downloadMissingLoras": "不足している LoRA をダウンロード",
|
||||
"clear": "選択をクリア",
|
||||
"skipMetadataRefreshCount": "スキップ({count}モデル)",
|
||||
"resumeMetadataRefreshCount": "再開({count}モデル)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "ベースモデルを更新",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "不足している LoRA をダウンロード",
|
||||
"message": "選択したレシピから合計 {totalCount} 個中 {uniqueCount} 個のユニークな不足している LoRA が見つかりました。",
|
||||
"previewTitle": "ダウンロードする LoRA:",
|
||||
"moreItems": "...あと {count} 個",
|
||||
"note": "ファイルはデフォルトのパステンプレートを使用してダウンロードされます。LoRA の数によっては時間がかかる場合があります。",
|
||||
"downloadButton": "{count} 個の LoRA をダウンロード"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "ローカル例画像",
|
||||
"message": "このモデルのローカル例画像が見つかりませんでした。表示オプション:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "レシピが選択されていません",
|
||||
"noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした",
|
||||
"noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。"
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "モデルが選択されていません",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기",
|
||||
"resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개",
|
||||
"deleteAll": "모든 모델 삭제",
|
||||
"downloadMissingLoras": "누락된 LoRA 다운로드",
|
||||
"clear": "선택 지우기",
|
||||
"skipMetadataRefreshCount": "건너뛰기({count}개 모델)",
|
||||
"resumeMetadataRefreshCount": "재개({count}개 모델)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "베이스 모델 업데이트",
|
||||
"cancel": "취소"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "누락된 LoRA 다운로드",
|
||||
"message": "선택한 레시피에서 총 {totalCount}개 중 {uniqueCount}개의 고유한 누락된 LoRA를 찾았습니다.",
|
||||
"previewTitle": "다운로드할 LoRA:",
|
||||
"moreItems": "...그리고 {count}개 더",
|
||||
"note": "파일은 기본 경로 템플릿을 사용하여 다운로드됩니다. LoRA의 수에 따라 다소 시간이 걸릴 수 있습니다.",
|
||||
"downloadButton": "{count}개 LoRA 다운로드"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "로컬 예시 이미지",
|
||||
"message": "이 모델의 로컬 예시 이미지를 찾을 수 없습니다. 보기 옵션:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "선택한 레시피가 없습니다",
|
||||
"noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다",
|
||||
"noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "선택된 모델이 없습니다",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "Пропустить обновление метаданных для выбранных",
|
||||
"resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных",
|
||||
"deleteAll": "Удалить все модели",
|
||||
"downloadMissingLoras": "Скачать отсутствующие LoRAs",
|
||||
"clear": "Очистить выбор",
|
||||
"skipMetadataRefreshCount": "Пропустить({count} моделей)",
|
||||
"resumeMetadataRefreshCount": "Возобновить({count} моделей)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "Обновить базовую модель",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "Скачать отсутствующие LoRAs",
|
||||
"message": "Найдено {uniqueCount} уникальных отсутствующих LoRAs (из {totalCount} всего в выбранных рецептах).",
|
||||
"previewTitle": "LoRAs для скачивания:",
|
||||
"moreItems": "...и еще {count}",
|
||||
"note": "Файлы будут скачаны с использованием шаблонов путей по умолчанию. Это может занять некоторое время в зависимости от количества LoRAs.",
|
||||
"downloadButton": "Скачать {count} LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "Локальные примеры изображений",
|
||||
"message": "Локальные примеры изображений для этой модели не найдены. Варианты просмотра:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||
"batchImportNoDirectory": "Please enter a directory path",
|
||||
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||
"noRecipesSelected": "Рецепты не выбраны",
|
||||
"noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs",
|
||||
"noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках."
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "Модели не выбраны",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "跳过所选模型的元数据刷新",
|
||||
"resumeMetadataRefresh": "恢复所选模型的元数据刷新",
|
||||
"deleteAll": "删除选中模型",
|
||||
"downloadMissingLoras": "下载缺失的 LoRAs",
|
||||
"clear": "清除选择",
|
||||
"skipMetadataRefreshCount": "跳过({count} 个模型)",
|
||||
"resumeMetadataRefreshCount": "恢复({count} 个模型)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "更新基础模型",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "下载缺失的 LoRAs",
|
||||
"message": "发现 {uniqueCount} 个独特的缺失 LoRAs(从选定配方中的 {totalCount} 个总数)。",
|
||||
"previewTitle": "要下载的 LoRAs:",
|
||||
"moreItems": "...还有 {count} 个",
|
||||
"note": "文件将使用默认路径模板下载。根据 LoRAs 的数量,这可能需要一些时间。",
|
||||
"downloadButton": "下载 {count} 个 LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "本地示例图片",
|
||||
"message": "未找到此模型的本地示例图片。可选操作:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "请输入至少一个 URL 或文件路径",
|
||||
"batchImportNoDirectory": "请输入目录路径",
|
||||
"batchImportBrowseFailed": "浏览目录失败:{message}",
|
||||
"batchImportDirectorySelected": "已选择目录:{path}"
|
||||
"batchImportDirectorySelected": "已选择目录:{path}",
|
||||
"noRecipesSelected": "未选择任何配方",
|
||||
"noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs",
|
||||
"noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。"
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "未选中模型",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"skipMetadataRefresh": "跳過所選模型的元數據更新",
|
||||
"resumeMetadataRefresh": "恢復所選模型的元數據更新",
|
||||
"deleteAll": "刪除全部模型",
|
||||
"downloadMissingLoras": "下載缺失的 LoRAs",
|
||||
"clear": "清除選取",
|
||||
"skipMetadataRefreshCount": "跳過({count} 個模型)",
|
||||
"resumeMetadataRefreshCount": "恢復({count} 個模型)",
|
||||
@@ -983,6 +984,14 @@
|
||||
"save": "更新基礎模型",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"bulkDownloadMissingLoras": {
|
||||
"title": "下載缺失的 LoRAs",
|
||||
"message": "發現 {uniqueCount} 個獨特的缺失 LoRAs(從選取食譜中的 {totalCount} 個總數)。",
|
||||
"previewTitle": "要下載的 LoRAs:",
|
||||
"moreItems": "...還有 {count} 個",
|
||||
"note": "檔案將使用預設路徑模板下載。根據 LoRAs 的數量,這可能需要一些時間。",
|
||||
"downloadButton": "下載 {count} 個 LoRA(s)"
|
||||
},
|
||||
"exampleAccess": {
|
||||
"title": "本機範例圖片",
|
||||
"message": "此模型未找到本機範例圖片。可選擇:",
|
||||
@@ -1507,7 +1516,10 @@
|
||||
"batchImportNoUrls": "請輸入至少一個 URL 或檔案路徑",
|
||||
"batchImportNoDirectory": "請輸入目錄路徑",
|
||||
"batchImportBrowseFailed": "瀏覽目錄失敗:{message}",
|
||||
"batchImportDirectorySelected": "已選擇目錄:{path}"
|
||||
"batchImportDirectorySelected": "已選擇目錄:{path}",
|
||||
"noRecipesSelected": "未選取任何食譜",
|
||||
"noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs",
|
||||
"noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。"
|
||||
},
|
||||
"models": {
|
||||
"noModelsSelected": "未選擇模型",
|
||||
|
||||
@@ -151,7 +151,8 @@ body.modal-open {
|
||||
[data-theme="dark"] .changelog-section,
|
||||
[data-theme="dark"] .update-info,
|
||||
[data-theme="dark"] .info-item,
|
||||
[data-theme="dark"] .path-preview {
|
||||
[data-theme="dark"] .path-preview,
|
||||
[data-theme="dark"] #bulkDownloadMissingLorasModal .bulk-download-loras-preview {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
@@ -349,3 +350,87 @@ button:disabled,
|
||||
margin-top: var(--space-1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Bulk Download Missing LoRAs Modal */
|
||||
#bulkDownloadMissingLorasModal .modal-body {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .confirmation-message {
|
||||
color: var(--text-color);
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .bulk-download-loras-preview {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .preview-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-color);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .bulk-download-loras-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .bulk-download-loras-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-1) 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .bulk-download-loras-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .bulk-download-loras-list li.more-items {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .lora-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .lora-version {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
margin-left: var(--space-1);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .confirmation-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#bulkDownloadMissingLorasModal .confirmation-note i {
|
||||
color: var(--lora-accent);
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { BaseContextMenu } from './BaseContextMenu.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { bulkManager } from '../../managers/BulkManager.js';
|
||||
import { updateElementText, translate } from '../../utils/i18nHelpers.js';
|
||||
import { bulkMissingLoraDownloadManager } from '../../managers/BulkMissingLoraDownloadManager.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
|
||||
export class BulkContextMenu extends BaseContextMenu {
|
||||
constructor() {
|
||||
@@ -37,6 +39,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||
const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]');
|
||||
|
||||
if (sendToWorkflowAppendItem) {
|
||||
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
||||
@@ -71,6 +74,10 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (setContentRatingItem) {
|
||||
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none';
|
||||
}
|
||||
if (downloadMissingLorasItem) {
|
||||
// Only show for recipes page
|
||||
downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
const skipMetadataRefreshItem = this.menu.querySelector('[data-action="skip-metadata-refresh"]');
|
||||
const resumeMetadataRefreshItem = this.menu.querySelector('[data-action="resume-metadata-refresh"]');
|
||||
@@ -178,6 +185,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'delete-all':
|
||||
bulkManager.showBulkDeleteModal();
|
||||
break;
|
||||
case 'download-missing-loras':
|
||||
this.handleDownloadMissingLoras();
|
||||
break;
|
||||
case 'clear':
|
||||
bulkManager.clearSelection();
|
||||
break;
|
||||
@@ -185,4 +195,39 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
console.warn(`Unknown bulk action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle downloading missing LoRAs for selected recipes
|
||||
*/
|
||||
async handleDownloadMissingLoras() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected recipes from the virtual scroller
|
||||
const selectedRecipes = [];
|
||||
state.selectedModels.forEach(filePath => {
|
||||
const card = document.querySelector(`.model-card[data-filepath="${CSS.escape(filePath)}"]`);
|
||||
if (card && card.recipeData) {
|
||||
selectedRecipes.push(card.recipeData);
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedRecipes.length === 0) {
|
||||
// Try to get recipes from virtual scroller state
|
||||
const items = state.virtualScroller?.items || [];
|
||||
items.forEach(recipe => {
|
||||
if (recipe.file_path && state.selectedModels.has(recipe.file_path)) {
|
||||
selectedRecipes.push(recipe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedRecipes.length === 0) {
|
||||
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
await bulkMissingLoraDownloadManager.downloadMissingLoras(selectedRecipes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1299,7 +1299,6 @@ class RecipeModal {
|
||||
|
||||
// New method to navigate to the LoRAs page
|
||||
navigateToLorasPage(specificLoraIndex = null) {
|
||||
debugger;
|
||||
// Close the current modal
|
||||
modalManager.closeModal('recipeModal');
|
||||
|
||||
|
||||
357
static/js/managers/BulkMissingLoraDownloadManager.js
Normal file
357
static/js/managers/BulkMissingLoraDownloadManager.js
Normal file
@@ -0,0 +1,357 @@
|
||||
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 { state } from '../state/index.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
|
||||
/**
|
||||
* Manager for downloading missing LoRAs for selected recipes in bulk
|
||||
*/
|
||||
export class BulkMissingLoraDownloadManager {
|
||||
constructor() {
|
||||
this.loraApiClient = getModelApiClient(MODEL_TYPES.LORA);
|
||||
this.pendingLoras = [];
|
||||
this.pendingRecipes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect missing LoRAs from selected recipes with deduplication
|
||||
* @param {Array} selectedRecipes - Array of selected recipe objects
|
||||
* @returns {Object} - Object containing unique missing LoRAs and statistics
|
||||
*/
|
||||
collectMissingLoras(selectedRecipes) {
|
||||
const uniqueLoras = new Map(); // key: hash or modelVersionId, value: lora object
|
||||
const missingLorasByRecipe = new Map();
|
||||
let totalMissingCount = 0;
|
||||
|
||||
selectedRecipes.forEach(recipe => {
|
||||
const missingLoras = [];
|
||||
|
||||
if (recipe.loras && Array.isArray(recipe.loras)) {
|
||||
recipe.loras.forEach(lora => {
|
||||
// Only include LoRAs not in library and not deleted
|
||||
if (!lora.inLibrary && !lora.isDeleted) {
|
||||
const uniqueKey = lora.hash || lora.id || lora.modelVersionId;
|
||||
|
||||
if (uniqueKey && !uniqueLoras.has(uniqueKey)) {
|
||||
// Store the LoRA info
|
||||
uniqueLoras.set(uniqueKey, {
|
||||
...lora,
|
||||
modelId: lora.modelId || lora.model_id,
|
||||
id: lora.id || lora.modelVersionId,
|
||||
});
|
||||
}
|
||||
|
||||
missingLoras.push(lora);
|
||||
totalMissingCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (missingLoras.length > 0) {
|
||||
missingLorasByRecipe.set(recipe.id || recipe.file_path, {
|
||||
recipe,
|
||||
missingLoras
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
uniqueLoras: Array.from(uniqueLoras.values()),
|
||||
uniqueCount: uniqueLoras.size,
|
||||
totalMissingCount,
|
||||
missingLorasByRecipe
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show confirmation modal for downloading missing LoRAs
|
||||
* @param {Object} stats - Statistics about missing LoRAs
|
||||
* @returns {Promise<boolean>} - Whether user confirmed
|
||||
*/
|
||||
async showConfirmationModal(stats) {
|
||||
const { uniqueCount, totalMissingCount, uniqueLoras } = stats;
|
||||
|
||||
if (uniqueCount === 0) {
|
||||
showToast('toast.recipes.noMissingLoras', {}, 'info');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store pending data for confirmation
|
||||
this.pendingLoras = uniqueLoras;
|
||||
|
||||
// Update modal content
|
||||
const messageEl = document.getElementById('bulkDownloadMissingLorasMessage');
|
||||
const listEl = document.getElementById('bulkDownloadMissingLorasList');
|
||||
const confirmBtn = document.getElementById('bulkDownloadMissingLorasConfirmBtn');
|
||||
|
||||
if (messageEl) {
|
||||
messageEl.textContent = translate('modals.bulkDownloadMissingLoras.message', {
|
||||
uniqueCount,
|
||||
totalCount: totalMissingCount
|
||||
}, `Found ${uniqueCount} unique missing LoRAs (from ${totalMissingCount} total across selected recipes).`);
|
||||
}
|
||||
|
||||
if (listEl) {
|
||||
listEl.innerHTML = uniqueLoras.slice(0, 10).map(lora => `
|
||||
<li>
|
||||
<span class="lora-name">${lora.name || lora.file_name || 'Unknown'}</span>
|
||||
${lora.version ? `<span class="lora-version">${lora.version}</span>` : ''}
|
||||
</li>
|
||||
`).join('') +
|
||||
(uniqueLoras.length > 10 ? `
|
||||
<li class="more-items">${translate('modals.bulkDownloadMissingLoras.moreItems', { count: uniqueLoras.length - 10 }, `...and ${uniqueLoras.length - 10} more`)}</li>
|
||||
` : '');
|
||||
}
|
||||
|
||||
if (confirmBtn) {
|
||||
confirmBtn.innerHTML = `
|
||||
<i class="fas fa-download"></i>
|
||||
${translate('modals.bulkDownloadMissingLoras.downloadButton', { count: uniqueCount }, `Download ${uniqueCount} LoRA(s)`)}
|
||||
`;
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modalManager.showModal('bulkDownloadMissingLorasModal');
|
||||
|
||||
// Return a promise that will be resolved when user confirms or cancels
|
||||
return new Promise((resolve) => {
|
||||
this.confirmResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user confirms download in modal
|
||||
*/
|
||||
async confirmDownload() {
|
||||
modalManager.closeModal('bulkDownloadMissingLorasModal');
|
||||
|
||||
if (this.confirmResolve) {
|
||||
this.confirmResolve(true);
|
||||
this.confirmResolve = null;
|
||||
}
|
||||
|
||||
// Execute download
|
||||
await this.executeDownload(this.pendingLoras);
|
||||
this.pendingLoras = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Download missing LoRAs for selected recipes
|
||||
* @param {Array} selectedRecipes - Array of selected recipe objects
|
||||
*/
|
||||
async downloadMissingLoras(selectedRecipes) {
|
||||
if (!selectedRecipes || selectedRecipes.length === 0) {
|
||||
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store selected recipes
|
||||
this.pendingRecipes = selectedRecipes;
|
||||
|
||||
// Collect missing LoRAs with deduplication
|
||||
const stats = this.collectMissingLoras(selectedRecipes);
|
||||
|
||||
if (stats.uniqueCount === 0) {
|
||||
showToast('toast.recipes.noMissingLorasInSelection', {}, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation modal
|
||||
const confirmed = await this.showConfirmationModal(stats);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the download process
|
||||
* @param {Array} lorasToDownload - Array of unique LoRAs to download
|
||||
*/
|
||||
async executeDownload(lorasToDownload) {
|
||||
const totalLoras = lorasToDownload.length;
|
||||
|
||||
// Get LoRA root directory
|
||||
const loraRoot = await this.getLoraRoot();
|
||||
if (!loraRoot) {
|
||||
showToast('toast.recipes.noLoraRootConfigured', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate batch download ID
|
||||
const batchDownloadId = Date.now().toString();
|
||||
|
||||
// Use default paths
|
||||
const useDefaultPaths = true;
|
||||
|
||||
// Set up WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${batchDownloadId}`);
|
||||
|
||||
// Show download progress UI
|
||||
const loadingManager = state.loadingManager;
|
||||
const updateProgress = loadingManager.showDownloadProgress(totalLoras);
|
||||
|
||||
let completedDownloads = 0;
|
||||
let failedDownloads = 0;
|
||||
let currentLoraProgress = 0;
|
||||
|
||||
// Set up WebSocket message handler
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle download ID confirmation
|
||||
if (data.type === 'download_id') {
|
||||
console.log(`Connected to batch download progress with ID: ${data.download_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process progress updates
|
||||
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
||||
currentLoraProgress = data.progress;
|
||||
|
||||
const currentLora = lorasToDownload[completedDownloads + failedDownloads];
|
||||
const loraName = currentLora ? (currentLora.name || currentLora.file_name || 'Unknown') : '';
|
||||
|
||||
const metrics = {
|
||||
bytesDownloaded: data.bytes_downloaded,
|
||||
totalBytes: data.total_bytes,
|
||||
bytesPerSecond: data.bytes_per_second
|
||||
};
|
||||
|
||||
updateProgress(currentLoraProgress, completedDownloads, loraName, metrics);
|
||||
|
||||
// Update status message
|
||||
if (currentLoraProgress < 3) {
|
||||
loadingManager.setStatus(
|
||||
translate('recipes.controls.import.startingDownload',
|
||||
{ current: completedDownloads + failedDownloads + 1, total: totalLoras },
|
||||
`Starting download for LoRA ${completedDownloads + failedDownloads + 1}/${totalLoras}`
|
||||
)
|
||||
);
|
||||
} else if (currentLoraProgress > 3 && currentLoraProgress < 100) {
|
||||
loadingManager.setStatus(
|
||||
translate('recipes.controls.import.downloadingLoras', {}, `Downloading LoRAs...`)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for WebSocket to connect
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
// Download each LoRA sequentially
|
||||
for (let i = 0; i < lorasToDownload.length; i++) {
|
||||
const lora = lorasToDownload[i];
|
||||
|
||||
currentLoraProgress = 0;
|
||||
|
||||
loadingManager.setStatus(
|
||||
translate('recipes.controls.import.startingDownload',
|
||||
{ current: i + 1, total: totalLoras },
|
||||
`Starting download for LoRA ${i + 1}/${totalLoras}`
|
||||
)
|
||||
);
|
||||
updateProgress(0, completedDownloads, lora.name || lora.file_name || 'Unknown');
|
||||
|
||||
try {
|
||||
const modelId = lora.modelId || lora.model_id;
|
||||
const versionId = lora.id || lora.modelVersionId;
|
||||
|
||||
if (!modelId && !versionId) {
|
||||
console.warn(`Skipping LoRA without model/version ID:`, lora);
|
||||
failedDownloads++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await this.loraApiClient.downloadModel(
|
||||
modelId,
|
||||
versionId,
|
||||
loraRoot,
|
||||
'', // Empty relative path, use default paths
|
||||
useDefaultPaths,
|
||||
batchDownloadId
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
console.error(`Failed to download LoRA ${lora.name || lora.file_name}: ${response.error}`);
|
||||
failedDownloads++;
|
||||
} else {
|
||||
completedDownloads++;
|
||||
updateProgress(100, completedDownloads, '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error downloading LoRA ${lora.name || lora.file_name}:`, error);
|
||||
failedDownloads++;
|
||||
}
|
||||
}
|
||||
|
||||
// Close WebSocket
|
||||
ws.close();
|
||||
|
||||
// Hide loading UI
|
||||
loadingManager.hide();
|
||||
|
||||
// Show completion message
|
||||
if (failedDownloads === 0) {
|
||||
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||
} else {
|
||||
showToast('toast.loras.downloadPartialSuccess', {
|
||||
completed: completedDownloads,
|
||||
total: totalLoras
|
||||
}, 'warning');
|
||||
}
|
||||
|
||||
// Refresh the recipes list to update LoRA status
|
||||
if (window.recipeManager) {
|
||||
window.recipeManager.loadRecipes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LoRA root directory from API
|
||||
* @returns {Promise<string|null>} - LoRA root directory or null
|
||||
*/
|
||||
async getLoraRoot() {
|
||||
try {
|
||||
// Fetch available LoRA roots from API
|
||||
const rootsData = await this.loraApiClient.fetchModelRoots();
|
||||
|
||||
if (!rootsData || !rootsData.roots || rootsData.roots.length === 0) {
|
||||
console.error('No LoRA roots available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to get default root from settings
|
||||
const defaultRootKey = 'default_lora_root';
|
||||
const defaultRoot = state.global?.settings?.[defaultRootKey];
|
||||
|
||||
// If default root is set and exists in available roots, use it
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
return defaultRoot;
|
||||
}
|
||||
|
||||
// Otherwise, return the first available root
|
||||
return rootsData.roots[0];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting LoRA root:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const bulkMissingLoraDownloadManager = new BulkMissingLoraDownloadManager();
|
||||
|
||||
// Make available globally for HTML onclick handlers
|
||||
if (typeof window !== 'undefined') {
|
||||
window.bulkMissingLoraDownloadManager = bulkMissingLoraDownloadManager;
|
||||
}
|
||||
@@ -291,6 +291,19 @@ export class ModalManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Register bulkDownloadMissingLorasModal
|
||||
const bulkDownloadMissingLorasModal = document.getElementById('bulkDownloadMissingLorasModal');
|
||||
if (bulkDownloadMissingLorasModal) {
|
||||
this.registerModal('bulkDownloadMissingLorasModal', {
|
||||
element: bulkDownloadMissingLorasModal,
|
||||
onClose: () => {
|
||||
this.getModal('bulkDownloadMissingLorasModal').element.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
},
|
||||
closeOnOutsideClick: true
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.boundHandleEscape);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
<i class="fas fa-redo"></i> <span>{{ t('loras.bulkOperations.resumeMetadataRefresh') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item" data-action="download-missing-loras">
|
||||
<i class="fas fa-download"></i> <span>{{ t('loras.bulkOperations.downloadMissingLoras') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="move-all">
|
||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||
</div>
|
||||
|
||||
@@ -80,4 +80,32 @@
|
||||
<button class="primary-btn" data-action="confirm-check-updates">{{ t('modals.checkUpdates.action') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Download Missing LoRAs Confirmation Modal -->
|
||||
<div id="bulkDownloadMissingLorasModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>{{ t('modals.bulkDownloadMissingLoras.title') }}</h2>
|
||||
<span class="close" onclick="modalManager.closeModal('bulkDownloadMissingLorasModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="confirmation-message" id="bulkDownloadMissingLorasMessage"></p>
|
||||
<div class="bulk-download-loras-preview" id="bulkDownloadMissingLorasPreview">
|
||||
<p class="preview-title">{{ t('modals.bulkDownloadMissingLoras.previewTitle') }}</p>
|
||||
<ul class="bulk-download-loras-list" id="bulkDownloadMissingLorasList"></ul>
|
||||
</div>
|
||||
<p class="confirmation-note">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{{ t('modals.bulkDownloadMissingLoras.note') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="modalManager.closeModal('bulkDownloadMissingLorasModal')">{{ t('common.actions.cancel') }}</button>
|
||||
<button class="primary-btn" id="bulkDownloadMissingLorasConfirmBtn" onclick="bulkMissingLoraDownloadManager.confirmDownload()">
|
||||
<i class="fas fa-download"></i>
|
||||
{{ t('modals.bulkDownloadMissingLoras.downloadButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user