mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
test(routes): cover snake case model id payload
This commit is contained in:
@@ -443,6 +443,7 @@
|
|||||||
"setContentRating": "Inhaltsbewertung für alle festlegen",
|
"setContentRating": "Inhaltsbewertung für alle festlegen",
|
||||||
"copyAll": "Alle Syntax kopieren",
|
"copyAll": "Alle Syntax kopieren",
|
||||||
"refreshAll": "Alle Metadaten aktualisieren",
|
"refreshAll": "Alle Metadaten aktualisieren",
|
||||||
|
"checkUpdates": "Auswahl auf Updates prüfen",
|
||||||
"moveAll": "Alle in Ordner verschieben",
|
"moveAll": "Alle in Ordner verschieben",
|
||||||
"autoOrganize": "Automatisch organisieren",
|
"autoOrganize": "Automatisch organisieren",
|
||||||
"deleteAll": "Alle Modelle löschen",
|
"deleteAll": "Alle Modelle löschen",
|
||||||
@@ -1206,6 +1207,12 @@
|
|||||||
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
|
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
|
||||||
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
|
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
|
||||||
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden",
|
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden",
|
||||||
|
"bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...",
|
||||||
|
"bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar",
|
||||||
|
"bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden",
|
||||||
|
"bulkUpdatesMissing": "Ausgewählte {type}-Modelle sind nicht mit Civitai-Updates verknüpft",
|
||||||
|
"bulkUpdatesPartialMissing": "{missing} ausgewählte {type}-Modelle ohne Civitai-Verknüpfung übersprungen",
|
||||||
|
"bulkUpdatesFailed": "Updates für ausgewählte {type}-Modelle konnten nicht geprüft werden: {message}",
|
||||||
"invalidCharactersRemoved": "Ungültige Zeichen aus Dateiname entfernt",
|
"invalidCharactersRemoved": "Ungültige Zeichen aus Dateiname entfernt",
|
||||||
"filenameCannotBeEmpty": "Dateiname darf nicht leer sein",
|
"filenameCannotBeEmpty": "Dateiname darf nicht leer sein",
|
||||||
"renameFailed": "Fehler beim Umbenennen der Datei: {message}",
|
"renameFailed": "Fehler beim Umbenennen der Datei: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "Set Content Rating for Selected",
|
"setContentRating": "Set Content Rating for Selected",
|
||||||
"copyAll": "Copy Selected Syntax",
|
"copyAll": "Copy Selected Syntax",
|
||||||
"refreshAll": "Refresh Selected Metadata",
|
"refreshAll": "Refresh Selected Metadata",
|
||||||
|
"checkUpdates": "Check Updates for Selected",
|
||||||
"moveAll": "Move Selected to Folder",
|
"moveAll": "Move Selected to Folder",
|
||||||
"autoOrganize": "Auto-Organize Selected",
|
"autoOrganize": "Auto-Organize Selected",
|
||||||
"deleteAll": "Delete Selected Models",
|
"deleteAll": "Delete Selected Models",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "Set content rating to {level} for {count} model(s)",
|
"bulkContentRatingSet": "Set content rating to {level} for {count} model(s)",
|
||||||
"bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
|
"bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
|
||||||
"bulkContentRatingFailed": "Failed to update content rating for selected models",
|
"bulkContentRatingFailed": "Failed to update content rating for selected models",
|
||||||
|
"bulkUpdatesChecking": "Checking selected {type}(s) for updates...",
|
||||||
|
"bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)",
|
||||||
|
"bulkUpdatesNone": "No updates found for selected {type}(s)",
|
||||||
|
"bulkUpdatesMissing": "Selected {type}(s) are not linked to Civitai updates",
|
||||||
|
"bulkUpdatesPartialMissing": "Skipped {missing} selected {type}(s) without Civitai links",
|
||||||
|
"bulkUpdatesFailed": "Failed to check updates for selected {type}(s): {message}",
|
||||||
"invalidCharactersRemoved": "Invalid characters removed from filename",
|
"invalidCharactersRemoved": "Invalid characters removed from filename",
|
||||||
"filenameCannotBeEmpty": "File name cannot be empty",
|
"filenameCannotBeEmpty": "File name cannot be empty",
|
||||||
"renameFailed": "Failed to rename file: {message}",
|
"renameFailed": "Failed to rename file: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "Establecer clasificación de contenido para todos",
|
"setContentRating": "Establecer clasificación de contenido para todos",
|
||||||
"copyAll": "Copiar toda la sintaxis",
|
"copyAll": "Copiar toda la sintaxis",
|
||||||
"refreshAll": "Actualizar todos los metadatos",
|
"refreshAll": "Actualizar todos los metadatos",
|
||||||
|
"checkUpdates": "Comprobar actualizaciones para la selección",
|
||||||
"moveAll": "Mover todos a carpeta",
|
"moveAll": "Mover todos a carpeta",
|
||||||
"autoOrganize": "Auto-organizar seleccionados",
|
"autoOrganize": "Auto-organizar seleccionados",
|
||||||
"deleteAll": "Eliminar todos los modelos",
|
"deleteAll": "Eliminar todos los modelos",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)",
|
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)",
|
||||||
"bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron",
|
"bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron",
|
||||||
"bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados",
|
"bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados",
|
||||||
|
"bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...",
|
||||||
|
"bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados",
|
||||||
|
"bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados",
|
||||||
|
"bulkUpdatesMissing": "Los {type} seleccionados no están vinculados a actualizaciones de Civitai",
|
||||||
|
"bulkUpdatesPartialMissing": "Se omitieron {missing} {type} seleccionados sin enlace de Civitai",
|
||||||
|
"bulkUpdatesFailed": "Error al comprobar actualizaciones para los {type} seleccionados: {message}",
|
||||||
"invalidCharactersRemoved": "Caracteres inválidos eliminados del nombre de archivo",
|
"invalidCharactersRemoved": "Caracteres inválidos eliminados del nombre de archivo",
|
||||||
"filenameCannotBeEmpty": "El nombre de archivo no puede estar vacío",
|
"filenameCannotBeEmpty": "El nombre de archivo no puede estar vacío",
|
||||||
"renameFailed": "Error al renombrar archivo: {message}",
|
"renameFailed": "Error al renombrar archivo: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "Définir la classification du contenu pour tous",
|
"setContentRating": "Définir la classification du contenu pour tous",
|
||||||
"copyAll": "Copier toute la syntaxe",
|
"copyAll": "Copier toute la syntaxe",
|
||||||
"refreshAll": "Actualiser toutes les métadonnées",
|
"refreshAll": "Actualiser toutes les métadonnées",
|
||||||
|
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
||||||
"moveAll": "Déplacer tout vers un dossier",
|
"moveAll": "Déplacer tout vers un dossier",
|
||||||
"autoOrganize": "Auto-organiser la sélection",
|
"autoOrganize": "Auto-organiser la sélection",
|
||||||
"deleteAll": "Supprimer tous les modèles",
|
"deleteAll": "Supprimer tous les modèles",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)",
|
"bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)",
|
||||||
"bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)",
|
"bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)",
|
||||||
"bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés",
|
"bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés",
|
||||||
|
"bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...",
|
||||||
|
"bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {type} sélectionnés",
|
||||||
|
"bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés",
|
||||||
|
"bulkUpdatesMissing": "Les {type} sélectionnés ne sont pas liés aux mises à jour Civitai",
|
||||||
|
"bulkUpdatesPartialMissing": "{missing} {type} sélectionnés sans lien Civitai ignorés",
|
||||||
|
"bulkUpdatesFailed": "Échec de la vérification des mises à jour pour les {type} sélectionnés : {message}",
|
||||||
"invalidCharactersRemoved": "Caractères invalides supprimés du nom de fichier",
|
"invalidCharactersRemoved": "Caractères invalides supprimés du nom de fichier",
|
||||||
"filenameCannotBeEmpty": "Le nom de fichier ne peut pas être vide",
|
"filenameCannotBeEmpty": "Le nom de fichier ne peut pas être vide",
|
||||||
"renameFailed": "Échec du renommage du fichier : {message}",
|
"renameFailed": "Échec du renommage du fichier : {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "הגדר דירוג תוכן לכל המודלים",
|
"setContentRating": "הגדר דירוג תוכן לכל המודלים",
|
||||||
"copyAll": "העתק את כל התחבירים",
|
"copyAll": "העתק את כל התחבירים",
|
||||||
"refreshAll": "רענן את כל המטא-דאטה",
|
"refreshAll": "רענן את כל המטא-דאטה",
|
||||||
|
"checkUpdates": "בדוק עדכונים לבחירה",
|
||||||
"moveAll": "העבר הכל לתיקייה",
|
"moveAll": "העבר הכל לתיקייה",
|
||||||
"autoOrganize": "ארגן אוטומטית נבחרים",
|
"autoOrganize": "ארגן אוטומטית נבחרים",
|
||||||
"deleteAll": "מחק את כל המודלים",
|
"deleteAll": "מחק את כל המודלים",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
|
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
|
||||||
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
|
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
|
||||||
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
|
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
|
||||||
|
"bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...",
|
||||||
|
"bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו",
|
||||||
|
"bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו",
|
||||||
|
"bulkUpdatesMissing": "ה-{type} שנבחרו אינם מקושרים לעדכוני Civitai",
|
||||||
|
"bulkUpdatesPartialMissing": "דילג על {missing} {type} שנבחרו ללא קישור Civitai",
|
||||||
|
"bulkUpdatesFailed": "בדיקת העדכונים עבור {type} שנבחרו נכשלה: {message}",
|
||||||
"invalidCharactersRemoved": "תווים לא חוקיים הוסרו משם הקובץ",
|
"invalidCharactersRemoved": "תווים לא חוקיים הוסרו משם הקובץ",
|
||||||
"filenameCannotBeEmpty": "שם הקובץ אינו יכול להיות ריק",
|
"filenameCannotBeEmpty": "שם הקובץ אינו יכול להיות ריק",
|
||||||
"renameFailed": "שינוי שם הקובץ נכשל: {message}",
|
"renameFailed": "שינוי שם הקובץ נכשל: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "すべてのモデルのコンテンツレーティングを設定",
|
"setContentRating": "すべてのモデルのコンテンツレーティングを設定",
|
||||||
"copyAll": "すべての構文をコピー",
|
"copyAll": "すべての構文をコピー",
|
||||||
"refreshAll": "すべてのメタデータを更新",
|
"refreshAll": "すべてのメタデータを更新",
|
||||||
|
"checkUpdates": "選択項目の更新を確認",
|
||||||
"moveAll": "すべてをフォルダに移動",
|
"moveAll": "すべてをフォルダに移動",
|
||||||
"autoOrganize": "自動整理を実行",
|
"autoOrganize": "自動整理を実行",
|
||||||
"deleteAll": "すべてのモデルを削除",
|
"deleteAll": "すべてのモデルを削除",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
|
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
|
||||||
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
|
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
|
||||||
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
|
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
|
||||||
|
"bulkUpdatesChecking": "選択された{type}の更新を確認しています...",
|
||||||
|
"bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります",
|
||||||
|
"bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした",
|
||||||
|
"bulkUpdatesMissing": "選択された{type}はCivitaiの更新にリンクされていません",
|
||||||
|
"bulkUpdatesPartialMissing": "Civitaiリンクがない{missing} 件の{type}をスキップしました",
|
||||||
|
"bulkUpdatesFailed": "選択された{type}の更新確認に失敗しました: {message}",
|
||||||
"invalidCharactersRemoved": "ファイル名から無効な文字が削除されました",
|
"invalidCharactersRemoved": "ファイル名から無効な文字が削除されました",
|
||||||
"filenameCannotBeEmpty": "ファイル名を空にすることはできません",
|
"filenameCannotBeEmpty": "ファイル名を空にすることはできません",
|
||||||
"renameFailed": "ファイル名の変更に失敗しました:{message}",
|
"renameFailed": "ファイル名の変更に失敗しました:{message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "모든 모델에 콘텐츠 등급 설정",
|
"setContentRating": "모든 모델에 콘텐츠 등급 설정",
|
||||||
"copyAll": "모든 문법 복사",
|
"copyAll": "모든 문법 복사",
|
||||||
"refreshAll": "모든 메타데이터 새로고침",
|
"refreshAll": "모든 메타데이터 새로고침",
|
||||||
|
"checkUpdates": "선택 항목 업데이트 확인",
|
||||||
"moveAll": "모두 폴더로 이동",
|
"moveAll": "모두 폴더로 이동",
|
||||||
"autoOrganize": "자동 정리 선택",
|
"autoOrganize": "자동 정리 선택",
|
||||||
"deleteAll": "모든 모델 삭제",
|
"deleteAll": "모든 모델 삭제",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
|
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
|
||||||
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
|
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
|
||||||
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
|
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
|
||||||
|
"bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...",
|
||||||
|
"bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다",
|
||||||
|
"bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다",
|
||||||
|
"bulkUpdatesMissing": "선택한 {type}이 Civitai 업데이트에 연결되어 있지 않습니다",
|
||||||
|
"bulkUpdatesPartialMissing": "Civitai 링크가 없는 {missing}개의 {type}을 건너뛰었습니다",
|
||||||
|
"bulkUpdatesFailed": "선택한 {type}의 업데이트 확인에 실패했습니다: {message}",
|
||||||
"invalidCharactersRemoved": "파일명에서 잘못된 문자가 제거되었습니다",
|
"invalidCharactersRemoved": "파일명에서 잘못된 문자가 제거되었습니다",
|
||||||
"filenameCannotBeEmpty": "파일 이름은 비어있을 수 없습니다",
|
"filenameCannotBeEmpty": "파일 이름은 비어있을 수 없습니다",
|
||||||
"renameFailed": "파일 이름 변경 실패: {message}",
|
"renameFailed": "파일 이름 변경 실패: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "Установить рейтинг контента для всех",
|
"setContentRating": "Установить рейтинг контента для всех",
|
||||||
"copyAll": "Копировать весь синтаксис",
|
"copyAll": "Копировать весь синтаксис",
|
||||||
"refreshAll": "Обновить все метаданные",
|
"refreshAll": "Обновить все метаданные",
|
||||||
|
"checkUpdates": "Проверить обновления для выбранных",
|
||||||
"moveAll": "Переместить все в папку",
|
"moveAll": "Переместить все в папку",
|
||||||
"autoOrganize": "Автоматически организовать выбранные",
|
"autoOrganize": "Автоматически организовать выбранные",
|
||||||
"deleteAll": "Удалить все модели",
|
"deleteAll": "Удалить все модели",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
|
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
|
||||||
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
|
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
|
||||||
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
|
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
|
||||||
|
"bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...",
|
||||||
|
"bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}",
|
||||||
|
"bulkUpdatesNone": "Обновления для выбранных {type} не найдены",
|
||||||
|
"bulkUpdatesMissing": "Выбранные {type} не привязаны к обновлениям Civitai",
|
||||||
|
"bulkUpdatesPartialMissing": "Пропущено {missing} выбранных {type} без привязки Civitai",
|
||||||
|
"bulkUpdatesFailed": "Не удалось проверить обновления для выбранных {type}: {message}",
|
||||||
"invalidCharactersRemoved": "Недопустимые символы удалены из имени файла",
|
"invalidCharactersRemoved": "Недопустимые символы удалены из имени файла",
|
||||||
"filenameCannotBeEmpty": "Имя файла не может быть пустым",
|
"filenameCannotBeEmpty": "Имя файла не может быть пустым",
|
||||||
"renameFailed": "Не удалось переименовать файл: {message}",
|
"renameFailed": "Не удалось переименовать файл: {message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "为所选中设置内容评级",
|
"setContentRating": "为所选中设置内容评级",
|
||||||
"copyAll": "复制所选中语法",
|
"copyAll": "复制所选中语法",
|
||||||
"refreshAll": "刷新所选中元数据",
|
"refreshAll": "刷新所选中元数据",
|
||||||
|
"checkUpdates": "检查所选更新",
|
||||||
"moveAll": "移动所选中到文件夹",
|
"moveAll": "移动所选中到文件夹",
|
||||||
"autoOrganize": "自动整理所选模型",
|
"autoOrganize": "自动整理所选模型",
|
||||||
"deleteAll": "删除选中模型",
|
"deleteAll": "删除选中模型",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
|
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
|
||||||
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败",
|
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败",
|
||||||
"bulkContentRatingFailed": "未能更新所选模型的内容评级",
|
"bulkContentRatingFailed": "未能更新所选模型的内容评级",
|
||||||
|
"bulkUpdatesChecking": "正在检查所选 {type} 的更新...",
|
||||||
|
"bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新",
|
||||||
|
"bulkUpdatesNone": "所选 {type} 未发现更新",
|
||||||
|
"bulkUpdatesMissing": "所选 {type} 未关联 Civitai 更新",
|
||||||
|
"bulkUpdatesPartialMissing": "已跳过 {missing} 个未关联 Civitai 的所选 {type}",
|
||||||
|
"bulkUpdatesFailed": "检查所选 {type} 的更新失败:{message}",
|
||||||
"invalidCharactersRemoved": "文件名中的无效字符已移除",
|
"invalidCharactersRemoved": "文件名中的无效字符已移除",
|
||||||
"filenameCannotBeEmpty": "文件名不能为空",
|
"filenameCannotBeEmpty": "文件名不能为空",
|
||||||
"renameFailed": "重命名文件失败:{message}",
|
"renameFailed": "重命名文件失败:{message}",
|
||||||
|
|||||||
@@ -442,6 +442,7 @@
|
|||||||
"setContentRating": "為全部設定內容分級",
|
"setContentRating": "為全部設定內容分級",
|
||||||
"copyAll": "複製全部語法",
|
"copyAll": "複製全部語法",
|
||||||
"refreshAll": "刷新全部 metadata",
|
"refreshAll": "刷新全部 metadata",
|
||||||
|
"checkUpdates": "檢查所選更新",
|
||||||
"moveAll": "全部移動到資料夾",
|
"moveAll": "全部移動到資料夾",
|
||||||
"autoOrganize": "自動整理所選模型",
|
"autoOrganize": "自動整理所選模型",
|
||||||
"deleteAll": "刪除全部模型",
|
"deleteAll": "刪除全部模型",
|
||||||
@@ -1205,6 +1206,12 @@
|
|||||||
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
|
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
|
||||||
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗",
|
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗",
|
||||||
"bulkContentRatingFailed": "無法更新所選模型的內容分級",
|
"bulkContentRatingFailed": "無法更新所選模型的內容分級",
|
||||||
|
"bulkUpdatesChecking": "正在檢查所選 {type} 的更新...",
|
||||||
|
"bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新",
|
||||||
|
"bulkUpdatesNone": "所選 {type} 未找到更新",
|
||||||
|
"bulkUpdatesMissing": "所選 {type} 未連結 Civitai 更新",
|
||||||
|
"bulkUpdatesPartialMissing": "已略過 {missing} 個未連結 Civitai 的所選 {type}",
|
||||||
|
"bulkUpdatesFailed": "檢查所選 {type} 更新失敗:{message}",
|
||||||
"invalidCharactersRemoved": "已移除檔名中的無效字元",
|
"invalidCharactersRemoved": "已移除檔名中的無效字元",
|
||||||
"filenameCannotBeEmpty": "檔案名稱不可為空",
|
"filenameCannotBeEmpty": "檔案名稱不可為空",
|
||||||
"renameFailed": "重新命名檔案失敗:{message}",
|
"renameFailed": "重新命名檔案失敗:{message}",
|
||||||
|
|||||||
@@ -1048,6 +1048,21 @@ class ModelUpdateHandler:
|
|||||||
force_refresh = self._parse_bool(request.query.get("force")) or self._parse_bool(
|
force_refresh = self._parse_bool(request.query.get("force")) or self._parse_bool(
|
||||||
payload.get("force")
|
payload.get("force")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
raw_model_ids = payload.get("modelIds")
|
||||||
|
if raw_model_ids is None:
|
||||||
|
raw_model_ids = payload.get("model_ids")
|
||||||
|
|
||||||
|
target_model_ids: list[int] = []
|
||||||
|
if isinstance(raw_model_ids, (list, tuple, set)):
|
||||||
|
for value in raw_model_ids:
|
||||||
|
normalized = self._normalize_model_id(value)
|
||||||
|
if normalized is not None:
|
||||||
|
target_model_ids.append(normalized)
|
||||||
|
|
||||||
|
if target_model_ids:
|
||||||
|
target_model_ids = sorted(set(target_model_ids))
|
||||||
|
|
||||||
provider = await self._get_civitai_provider()
|
provider = await self._get_civitai_provider()
|
||||||
if provider is None:
|
if provider is None:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -1060,6 +1075,7 @@ class ModelUpdateHandler:
|
|||||||
self._service.scanner,
|
self._service.scanner,
|
||||||
provider,
|
provider,
|
||||||
force_refresh=force_refresh,
|
force_refresh=force_refresh,
|
||||||
|
target_model_ids=target_model_ids or None,
|
||||||
)
|
)
|
||||||
except RateLimitError as exc:
|
except RateLimitError as exc:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|||||||
@@ -210,15 +210,33 @@ class ModelUpdateService:
|
|||||||
metadata_provider,
|
metadata_provider,
|
||||||
*,
|
*,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
|
target_model_ids: Optional[Sequence[int]] = None,
|
||||||
) -> Dict[int, ModelUpdateRecord]:
|
) -> Dict[int, ModelUpdateRecord]:
|
||||||
"""Refresh update information for every model present in the cache."""
|
"""Refresh update information for every model present in the cache."""
|
||||||
|
|
||||||
local_versions = await self._collect_local_versions(scanner)
|
normalized_targets = (
|
||||||
|
self._normalize_sequence(target_model_ids)
|
||||||
|
if target_model_ids is not None
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
target_filter = normalized_targets or None
|
||||||
|
|
||||||
|
local_versions = await self._collect_local_versions(
|
||||||
|
scanner,
|
||||||
|
target_model_ids=target_filter,
|
||||||
|
)
|
||||||
total_models = len(local_versions)
|
total_models = len(local_versions)
|
||||||
if total_models == 0:
|
if total_models == 0:
|
||||||
logger.info(
|
if target_filter:
|
||||||
"No %s models found while refreshing update metadata", model_type
|
logger.info(
|
||||||
)
|
"No %s models matched requested ids %s while refreshing update metadata",
|
||||||
|
model_type,
|
||||||
|
target_filter,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"No %s models found while refreshing update metadata", model_type
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -616,12 +634,23 @@ class ModelUpdateService:
|
|||||||
)
|
)
|
||||||
return aggregated
|
return aggregated
|
||||||
|
|
||||||
async def _collect_local_versions(self, scanner) -> Dict[int, List[int]]:
|
async def _collect_local_versions(
|
||||||
|
self,
|
||||||
|
scanner,
|
||||||
|
*,
|
||||||
|
target_model_ids: Optional[Sequence[int]] = None,
|
||||||
|
) -> Dict[int, List[int]]:
|
||||||
cache = await scanner.get_cached_data()
|
cache = await scanner.get_cached_data()
|
||||||
mapping: Dict[int, set[int]] = {}
|
mapping: Dict[int, set[int]] = {}
|
||||||
if not cache or not getattr(cache, "raw_data", None):
|
if not cache or not getattr(cache, "raw_data", None):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
target_set = None
|
||||||
|
if target_model_ids:
|
||||||
|
target_set = set(target_model_ids)
|
||||||
|
if not target_set:
|
||||||
|
return {}
|
||||||
|
|
||||||
for item in cache.raw_data:
|
for item in cache.raw_data:
|
||||||
civitai = item.get("civitai") if isinstance(item, dict) else None
|
civitai = item.get("civitai") if isinstance(item, dict) else None
|
||||||
if not isinstance(civitai, dict):
|
if not isinstance(civitai, dict):
|
||||||
@@ -630,6 +659,8 @@ class ModelUpdateService:
|
|||||||
version_id = self._normalize_int(civitai.get("id"))
|
version_id = self._normalize_int(civitai.get("id"))
|
||||||
if model_id is None or version_id is None:
|
if model_id is None or version_id is None:
|
||||||
continue
|
continue
|
||||||
|
if target_set is not None and model_id not in target_set:
|
||||||
|
continue
|
||||||
mapping.setdefault(model_id, set()).add(version_id)
|
mapping.setdefault(model_id, set()).add(version_id)
|
||||||
|
|
||||||
return {model_id: sorted(ids) for model_id, ids in mapping.items()}
|
return {model_id: sorted(ids) for model_id, ids in mapping.items()}
|
||||||
|
|||||||
@@ -569,6 +569,35 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshUpdatesForModels(modelIds, { force = false } = {}) {
|
||||||
|
if (!Array.isArray(modelIds) || modelIds.length === 0) {
|
||||||
|
throw new Error('No model IDs provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_ids: modelIds,
|
||||||
|
force
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let payload = {};
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to parse refresh updates response as JSON', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || payload?.success !== true) {
|
||||||
|
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
async fetchCivitaiVersions(modelId, source = null) {
|
async fetchCivitaiVersions(modelId, source = null) {
|
||||||
try {
|
try {
|
||||||
let requestUrl = `${this.apiConfig.endpoints.civitaiVersions}/${modelId}`;
|
let requestUrl = `${this.apiConfig.endpoints.civitaiVersions}/${modelId}`;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]');
|
const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]');
|
||||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||||
|
const checkUpdatesItem = this.menu.querySelector('[data-action="check-updates"]');
|
||||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||||
@@ -49,6 +50,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
if (refreshAllItem) {
|
if (refreshAllItem) {
|
||||||
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (checkUpdatesItem) {
|
||||||
|
checkUpdatesItem.style.display = config.checkUpdates ? 'flex' : 'none';
|
||||||
|
}
|
||||||
if (moveAllItem) {
|
if (moveAllItem) {
|
||||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
@@ -105,6 +109,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
case 'refresh-all':
|
case 'refresh-all':
|
||||||
bulkManager.refreshAllMetadata();
|
bulkManager.refreshAllMetadata();
|
||||||
break;
|
break;
|
||||||
|
case 'check-updates':
|
||||||
|
bulkManager.checkUpdatesForSelectedModels();
|
||||||
|
break;
|
||||||
case 'move-all':
|
case 'move-all':
|
||||||
window.moveManager.showMoveModal('bulk');
|
window.moveManager.showMoveModal('bulk');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -436,6 +436,12 @@ export function createModelCard(model, modelType) {
|
|||||||
const hasUpdateAvailable = Boolean(model.update_available);
|
const hasUpdateAvailable = Boolean(model.update_available);
|
||||||
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
||||||
|
|
||||||
|
const civitaiData = model.civitai || {};
|
||||||
|
const modelId = civitaiData?.modelId ?? civitaiData?.model_id;
|
||||||
|
if (modelId !== undefined && modelId !== null && modelId !== '') {
|
||||||
|
card.dataset.modelId = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
// LoRA specific data
|
// LoRA specific data
|
||||||
if (modelType === MODEL_TYPES.LORA) {
|
if (modelType === MODEL_TYPES.LORA) {
|
||||||
card.dataset.usage_tips = model.usage_tips;
|
card.dataset.usage_tips = model.usage_tips;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
|||||||
sendToWorkflow: true,
|
sendToWorkflow: true,
|
||||||
copyAll: true,
|
copyAll: true,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
|
checkUpdates: true,
|
||||||
moveAll: true,
|
moveAll: true,
|
||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
@@ -44,6 +45,7 @@ export class BulkManager {
|
|||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
|
checkUpdates: true,
|
||||||
moveAll: true,
|
moveAll: true,
|
||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
@@ -54,6 +56,7 @@ export class BulkManager {
|
|||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
|
checkUpdates: true,
|
||||||
moveAll: false,
|
moveAll: false,
|
||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
@@ -273,12 +276,7 @@ export class BulkManager {
|
|||||||
state.selectedModels.add(filepath);
|
state.selectedModels.add(filepath);
|
||||||
|
|
||||||
// Cache the metadata for this model
|
// Cache the metadata for this model
|
||||||
const metadataCache = this.getMetadataCache();
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
metadataCache.set(filepath, {
|
|
||||||
fileName: card.dataset.file_name,
|
|
||||||
usageTips: card.dataset.usage_tips,
|
|
||||||
modelName: card.dataset.name
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update context menu header if visible
|
// Update context menu header if visible
|
||||||
@@ -305,6 +303,89 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseModelId(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMetadataCacheFromCard(filepath, card) {
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataCache = this.getMetadataCache();
|
||||||
|
const existing = metadataCache.get(filepath) || {};
|
||||||
|
const modelId = this.parseModelId(card.dataset.modelId);
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...existing,
|
||||||
|
fileName: card.dataset.file_name ?? existing.fileName,
|
||||||
|
usageTips: card.dataset.usage_tips ?? existing.usageTips,
|
||||||
|
modelName: card.dataset.name ?? existing.modelName,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (modelId !== null) {
|
||||||
|
updated.modelId = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataCache.set(filepath, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeAttributeValue(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
getModelIdForFilePath(filePath) {
|
||||||
|
const metadataCache = this.getMetadataCache();
|
||||||
|
const cached = metadataCache.get(filePath);
|
||||||
|
if (cached && typeof cached.modelId === 'number') {
|
||||||
|
return cached.modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedPath = this.escapeAttributeValue(filePath);
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
|
if (!card) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMetadataCacheFromCard(filePath, card);
|
||||||
|
const updated = metadataCache.get(filePath);
|
||||||
|
return updated && typeof updated.modelId === 'number' ? updated.modelId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectSelectedModelIds() {
|
||||||
|
const metadataCache = this.getMetadataCache();
|
||||||
|
const ids = [];
|
||||||
|
let missingCount = 0;
|
||||||
|
|
||||||
|
for (const filepath of state.selectedModels) {
|
||||||
|
const cached = metadataCache.get(filepath);
|
||||||
|
let modelId = cached && typeof cached.modelId === 'number' ? cached.modelId : null;
|
||||||
|
if (modelId === null) {
|
||||||
|
modelId = this.getModelIdForFilePath(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof modelId === 'number') {
|
||||||
|
ids.push(modelId);
|
||||||
|
} else {
|
||||||
|
missingCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueIds = Array.from(new Set(ids));
|
||||||
|
return { ids: uniqueIds, missingCount };
|
||||||
|
}
|
||||||
|
|
||||||
applySelectionState() {
|
applySelectionState() {
|
||||||
if (!state.bulkMode) return;
|
if (!state.bulkMode) return;
|
||||||
|
|
||||||
@@ -313,12 +394,7 @@ export class BulkManager {
|
|||||||
if (state.selectedModels.has(filepath)) {
|
if (state.selectedModels.has(filepath)) {
|
||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
|
|
||||||
const metadataCache = this.getMetadataCache();
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
metadataCache.set(filepath, {
|
|
||||||
fileName: card.dataset.file_name,
|
|
||||||
usageTips: card.dataset.usage_tips,
|
|
||||||
modelName: card.dataset.name
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
@@ -479,10 +555,12 @@ export class BulkManager {
|
|||||||
state.selectedModels.add(item.file_path);
|
state.selectedModels.add(item.file_path);
|
||||||
|
|
||||||
if (!metadataCache.has(item.file_path)) {
|
if (!metadataCache.has(item.file_path)) {
|
||||||
|
const modelId = this.parseModelId(item?.civitai?.modelId);
|
||||||
metadataCache.set(item.file_path, {
|
metadataCache.set(item.file_path, {
|
||||||
fileName: item.file_name,
|
fileName: item.file_name,
|
||||||
usageTips: item.usage_tips || '{}',
|
usageTips: item.usage_tips || '{}',
|
||||||
modelName: item.name || item.file_name
|
modelName: item.name || item.file_name,
|
||||||
|
...(modelId !== null ? { modelId } : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,12 +599,7 @@ export class BulkManager {
|
|||||||
if (metadata) {
|
if (metadata) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
metadataCache.set(filepath, {
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
...metadata,
|
|
||||||
fileName: card.dataset.file_name,
|
|
||||||
usageTips: card.dataset.usage_tips,
|
|
||||||
modelName: card.dataset.name
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -542,6 +615,70 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkUpdatesForSelectedModels() {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentType = state.currentPageType;
|
||||||
|
const currentConfig = MODEL_CONFIG[currentType] || MODEL_CONFIG[MODEL_TYPES.LORA];
|
||||||
|
const typeLabel = (currentConfig?.displayName || 'Model').toLowerCase();
|
||||||
|
|
||||||
|
const { ids: modelIds, missingCount } = this.collectSelectedModelIds();
|
||||||
|
|
||||||
|
if (modelIds.length === 0) {
|
||||||
|
showToast('toast.models.bulkUpdatesMissing', { type: typeLabel }, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingCount > 0) {
|
||||||
|
showToast('toast.models.bulkUpdatesPartialMissing', { missing: missingCount, type: typeLabel }, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
if (!apiClient || typeof apiClient.refreshUpdatesForModels !== 'function') {
|
||||||
|
console.warn('Model API client does not support refreshUpdatesForModels');
|
||||||
|
showToast('toast.models.bulkUpdatesFailed', { type: typeLabel, message: 'Operation not supported' }, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingMessage = translate(
|
||||||
|
'toast.models.bulkUpdatesChecking',
|
||||||
|
{ count: state.selectedModels.size, type: typeLabel },
|
||||||
|
`Checking selected ${typeLabel}(s) for updates...`
|
||||||
|
);
|
||||||
|
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.refreshUpdatesForModels(modelIds);
|
||||||
|
const records = Array.isArray(response?.records) ? response.records : [];
|
||||||
|
const updatesCount = records.length;
|
||||||
|
|
||||||
|
if (updatesCount > 0) {
|
||||||
|
showToast('toast.models.bulkUpdatesSuccess', { count: updatesCount, type: typeLabel }, 'success');
|
||||||
|
} else {
|
||||||
|
showToast('toast.models.bulkUpdatesNone', { type: typeLabel }, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetAndReload(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking updates for selected models:', error);
|
||||||
|
showToast(
|
||||||
|
'toast.models.bulkUpdatesFailed',
|
||||||
|
{ type: typeLabel, message: error?.message ?? 'Unknown error' },
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (state.loadingManager?.hide) {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
if (typeof state.loadingManager?.restoreProgressBar === 'function') {
|
||||||
|
state.loadingManager.restoreProgressBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showBulkAddTagsModal() {
|
showBulkAddTagsModal() {
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
@@ -1267,11 +1404,7 @@ export class BulkManager {
|
|||||||
// Cache metadata if not already cached
|
// Cache metadata if not already cached
|
||||||
const metadataCache = this.getMetadataCache();
|
const metadataCache = this.getMetadataCache();
|
||||||
if (!metadataCache.has(filepath)) {
|
if (!metadataCache.has(filepath)) {
|
||||||
metadataCache.set(filepath, {
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
fileName: card.dataset.file_name,
|
|
||||||
usageTips: card.dataset.usage_tips,
|
|
||||||
modelName: card.dataset.name
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (!this.initialSelectedModels.has(filepath)) {
|
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||||
// Remove from selection if not intersecting and wasn't initially selected
|
// Remove from selection if not intersecting and wasn't initially selected
|
||||||
|
|||||||
@@ -53,6 +53,9 @@
|
|||||||
<div class="context-menu-item" data-action="refresh-all">
|
<div class="context-menu-item" data-action="refresh-all">
|
||||||
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="check-updates">
|
||||||
|
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
|
||||||
|
</div>
|
||||||
<div class="context-menu-item" data-action="copy-all">
|
<div class="context-menu-item" data-action="copy-all">
|
||||||
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,13 +28,22 @@ class DummyUpdateService:
|
|||||||
self.records = records
|
self.records = records
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
async def refresh_for_model_type(self, model_type, scanner, provider, *, force_refresh=False):
|
async def refresh_for_model_type(
|
||||||
|
self,
|
||||||
|
model_type,
|
||||||
|
scanner,
|
||||||
|
provider,
|
||||||
|
*,
|
||||||
|
force_refresh=False,
|
||||||
|
target_model_ids=None,
|
||||||
|
):
|
||||||
self.calls.append(
|
self.calls.append(
|
||||||
{
|
{
|
||||||
"model_type": model_type,
|
"model_type": model_type,
|
||||||
"scanner": scanner,
|
"scanner": scanner,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"force_refresh": force_refresh,
|
"force_refresh": force_refresh,
|
||||||
|
"target_model_ids": target_model_ids,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.records
|
return self.records
|
||||||
@@ -152,3 +161,106 @@ async def test_refresh_model_updates_filters_records_without_updates():
|
|||||||
assert call["scanner"] is service.scanner
|
assert call["scanner"] is service.scanner
|
||||||
assert call["force_refresh"] is False
|
assert call["force_refresh"] is False
|
||||||
assert call["provider"] is not None
|
assert call["provider"] is not None
|
||||||
|
assert call["target_model_ids"] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_model_updates_with_target_ids():
|
||||||
|
cache = SimpleNamespace(version_index={})
|
||||||
|
service = DummyService(cache)
|
||||||
|
|
||||||
|
record_with_update = ModelUpdateRecord(
|
||||||
|
model_type="lora",
|
||||||
|
model_id=1,
|
||||||
|
versions=[
|
||||||
|
ModelVersionRecord(
|
||||||
|
version_id=10,
|
||||||
|
name="v1",
|
||||||
|
base_model=None,
|
||||||
|
released_at=None,
|
||||||
|
size_bytes=None,
|
||||||
|
preview_url=None,
|
||||||
|
is_in_library=False,
|
||||||
|
should_ignore=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
last_checked_at=None,
|
||||||
|
should_ignore_model=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_service = DummyUpdateService({1: record_with_update})
|
||||||
|
|
||||||
|
async def metadata_selector(name):
|
||||||
|
assert name == "civitai_api"
|
||||||
|
return object()
|
||||||
|
|
||||||
|
handler = ModelUpdateHandler(
|
||||||
|
service=service,
|
||||||
|
update_service=update_service,
|
||||||
|
metadata_provider_selector=metadata_selector,
|
||||||
|
logger=logging.getLogger(__name__),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DummyRequest:
|
||||||
|
can_read_body = True
|
||||||
|
query = {}
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return {"modelIds": [1, "2", None]}
|
||||||
|
|
||||||
|
response = await handler.refresh_model_updates(DummyRequest())
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
call = update_service.calls[0]
|
||||||
|
assert call["target_model_ids"] == [1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_model_updates_accepts_snake_case_ids():
|
||||||
|
cache = SimpleNamespace(version_index={})
|
||||||
|
service = DummyService(cache)
|
||||||
|
|
||||||
|
record_with_update = ModelUpdateRecord(
|
||||||
|
model_type="lora",
|
||||||
|
model_id=3,
|
||||||
|
versions=[
|
||||||
|
ModelVersionRecord(
|
||||||
|
version_id=30,
|
||||||
|
name="v3",
|
||||||
|
base_model=None,
|
||||||
|
released_at=None,
|
||||||
|
size_bytes=None,
|
||||||
|
preview_url=None,
|
||||||
|
is_in_library=False,
|
||||||
|
should_ignore=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
last_checked_at=None,
|
||||||
|
should_ignore_model=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_service = DummyUpdateService({3: record_with_update})
|
||||||
|
|
||||||
|
async def metadata_selector(name):
|
||||||
|
assert name == "civitai_api"
|
||||||
|
return object()
|
||||||
|
|
||||||
|
handler = ModelUpdateHandler(
|
||||||
|
service=service,
|
||||||
|
update_service=update_service,
|
||||||
|
metadata_provider_selector=metadata_selector,
|
||||||
|
logger=logging.getLogger(__name__),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DummyRequest:
|
||||||
|
can_read_body = True
|
||||||
|
query = {}
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return {"model_ids": [3, "4", "abc", None]}
|
||||||
|
|
||||||
|
response = await handler.refresh_model_updates(DummyRequest())
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
call = update_service.calls[0]
|
||||||
|
assert call["target_model_ids"] == [3, 4]
|
||||||
|
|||||||
@@ -143,6 +143,47 @@ async def test_refresh_persists_versions_and_uses_cache(tmp_path):
|
|||||||
assert provider.bulk_calls == [[1]]
|
assert provider.bulk_calls == [[1]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_filters_to_requested_models(tmp_path):
|
||||||
|
db_path = tmp_path / "updates.sqlite"
|
||||||
|
service = ModelUpdateService(str(db_path), ttl_seconds=3600)
|
||||||
|
raw_data = [
|
||||||
|
{"civitai": {"modelId": 1, "id": 11}},
|
||||||
|
{"civitai": {"modelId": 2, "id": 21}},
|
||||||
|
]
|
||||||
|
scanner = DummyScanner(raw_data)
|
||||||
|
provider = DummyProvider({"modelVersions": []})
|
||||||
|
|
||||||
|
result = await service.refresh_for_model_type(
|
||||||
|
"lora",
|
||||||
|
scanner,
|
||||||
|
provider,
|
||||||
|
target_model_ids=[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert list(result.keys()) == [2]
|
||||||
|
assert provider.bulk_calls == [[2]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_returns_empty_when_targets_missing(tmp_path):
|
||||||
|
db_path = tmp_path / "updates.sqlite"
|
||||||
|
service = ModelUpdateService(str(db_path), ttl_seconds=3600)
|
||||||
|
raw_data = [{"civitai": {"modelId": 1, "id": 11}}]
|
||||||
|
scanner = DummyScanner(raw_data)
|
||||||
|
provider = DummyProvider({"modelVersions": []})
|
||||||
|
|
||||||
|
result = await service.refresh_for_model_type(
|
||||||
|
"lora",
|
||||||
|
scanner,
|
||||||
|
provider,
|
||||||
|
target_model_ids=[5],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {}
|
||||||
|
assert provider.bulk_calls == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_refresh_respects_ignore_flag(tmp_path):
|
async def test_refresh_respects_ignore_flag(tmp_path):
|
||||||
db_path = tmp_path / "updates.sqlite"
|
db_path = tmp_path / "updates.sqlite"
|
||||||
|
|||||||
Reference in New Issue
Block a user