diff --git a/locales/de.json b/locales/de.json index 1b7386a0..64af9636 100644 --- a/locales/de.json +++ b/locales/de.json @@ -393,6 +393,7 @@ "viewSelected": "Auswahl anzeigen", "addTags": "Allen Tags hinzufügen", "setBaseModel": "Basis-Modell für alle festlegen", + "setContentRating": "Inhaltsbewertung für alle festlegen", "copyAll": "Alle Syntax kopieren", "refreshAll": "Alle Metadaten aktualisieren", "moveAll": "Alle in Ordner verschieben", @@ -617,6 +618,7 @@ "contentRating": { "title": "Inhaltsbewertung festlegen", "current": "Aktuell", + "multiple": "Mehrere Werte", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "Basis-Modell erfolgreich für {count} Modell(e) aktualisiert", "bulkBaseModelUpdatePartial": "{success} Modelle aktualisiert, {failed} fehlgeschlagen", "bulkBaseModelUpdateFailed": "Aktualisierung des Basis-Modells für ausgewählte Modelle fehlgeschlagen", + "bulkContentRatingUpdating": "Inhaltsbewertung wird für {count} Modell(e) aktualisiert...", + "bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt", + "bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen", + "bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden", "invalidCharactersRemoved": "Ungültige Zeichen aus Dateiname entfernt", "filenameCannotBeEmpty": "Dateiname darf nicht leer sein", "renameFailed": "Fehler beim Umbenennen der Datei: {message}", diff --git a/locales/en.json b/locales/en.json index 828ac4f8..23592daf 100644 --- a/locales/en.json +++ b/locales/en.json @@ -393,6 +393,7 @@ "viewSelected": "View Selected", "addTags": "Add Tags to All", "setBaseModel": "Set Base Model for All", + "setContentRating": "Set Content Rating for All", "copyAll": "Copy All Syntax", "refreshAll": "Refresh All Metadata", "moveAll": "Move All to Folder", @@ -617,6 +618,7 @@ "contentRating": { "title": "Set Content Rating", "current": "Current", + "multiple": "Multiple values", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "Successfully updated base model for {count} model(s)", "bulkBaseModelUpdatePartial": "Updated {success} model(s), failed {failed} model(s)", "bulkBaseModelUpdateFailed": "Failed to update base model for selected models", + "bulkContentRatingUpdating": "Updating content rating 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", + "bulkContentRatingFailed": "Failed to update content rating for selected models", "invalidCharactersRemoved": "Invalid characters removed from filename", "filenameCannotBeEmpty": "File name cannot be empty", "renameFailed": "Failed to rename file: {message}", diff --git a/locales/es.json b/locales/es.json index 80d1be69..37a58183 100644 --- a/locales/es.json +++ b/locales/es.json @@ -393,6 +393,7 @@ "viewSelected": "Ver seleccionados", "addTags": "Añadir etiquetas a todos", "setBaseModel": "Establecer modelo base para todos", + "setContentRating": "Establecer clasificación de contenido para todos", "copyAll": "Copiar toda la sintaxis", "refreshAll": "Actualizar todos los metadatos", "moveAll": "Mover todos a carpeta", @@ -617,6 +618,7 @@ "contentRating": { "title": "Establecer clasificación de contenido", "current": "Actual", + "multiple": "Valores múltiples", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "Modelo base actualizado exitosamente para {count} modelo(s)", "bulkBaseModelUpdatePartial": "Actualizados {success} modelo(s), fallaron {failed} modelo(s)", "bulkBaseModelUpdateFailed": "Error al actualizar el modelo base para los modelos seleccionados", + "bulkContentRatingUpdating": "Actualizando la clasificación de contenido 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", + "bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados", "invalidCharactersRemoved": "Caracteres inválidos eliminados del nombre de archivo", "filenameCannotBeEmpty": "El nombre de archivo no puede estar vacío", "renameFailed": "Error al renombrar archivo: {message}", diff --git a/locales/fr.json b/locales/fr.json index 4049b1fa..591dfcbe 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -393,6 +393,7 @@ "viewSelected": "Voir la sélection", "addTags": "Ajouter des tags à tous", "setBaseModel": "Définir le modèle de base pour tous", + "setContentRating": "Définir la classification du contenu pour tous", "copyAll": "Copier toute la syntaxe", "refreshAll": "Actualiser toutes les métadonnées", "moveAll": "Déplacer tout vers un dossier", @@ -617,6 +618,7 @@ "contentRating": { "title": "Définir la classification du contenu", "current": "Actuel", + "multiple": "Valeurs multiples", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "Modèle de base mis à jour avec succès pour {count} modèle(s)", "bulkBaseModelUpdatePartial": "{success} modèle(s) mis à jour, {failed} modèle(s) en échec", "bulkBaseModelUpdateFailed": "Échec de la mise à jour du modèle de base pour les modèles sélectionnés", + "bulkContentRatingUpdating": "Mise à jour de la classification du contenu 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)", + "bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés", "invalidCharactersRemoved": "Caractères invalides supprimés du nom de fichier", "filenameCannotBeEmpty": "Le nom de fichier ne peut pas être vide", "renameFailed": "Échec du renommage du fichier : {message}", diff --git a/locales/he.json b/locales/he.json index 814d6f10..1d089d39 100644 --- a/locales/he.json +++ b/locales/he.json @@ -393,6 +393,7 @@ "viewSelected": "הצג נבחרים", "addTags": "הוסף תגיות לכל", "setBaseModel": "הגדר מודל בסיס לכל", + "setContentRating": "הגדר דירוג תוכן לכל המודלים", "copyAll": "העתק את כל התחבירים", "refreshAll": "רענן את כל המטא-דאטה", "moveAll": "העבר הכל לתיקייה", @@ -617,6 +618,7 @@ "contentRating": { "title": "הגדר דירוג תוכן", "current": "נוכחי", + "multiple": "ערכים מרובים", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "עודכן בהצלחה מודל הבסיס עבור {count} מודל(ים)", "bulkBaseModelUpdatePartial": "עודכנו {success} מודל(ים), נכשלו {failed} מודל(ים)", "bulkBaseModelUpdateFailed": "עדכון מודל הבסיס עבור המודלים שנבחרו נכשל", + "bulkContentRatingUpdating": "מעדכן דירוג תוכן עבור {count} מודלים...", + "bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים", + "bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו", + "bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל", "invalidCharactersRemoved": "תווים לא חוקיים הוסרו משם הקובץ", "filenameCannotBeEmpty": "שם הקובץ אינו יכול להיות ריק", "renameFailed": "שינוי שם הקובץ נכשל: {message}", diff --git a/locales/ja.json b/locales/ja.json index 832664fb..972f78af 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -393,6 +393,7 @@ "viewSelected": "選択中を表示", "addTags": "すべてにタグを追加", "setBaseModel": "すべてにベースモデルを設定", + "setContentRating": "すべてのモデルのコンテンツレーティングを設定", "copyAll": "すべての構文をコピー", "refreshAll": "すべてのメタデータを更新", "moveAll": "すべてをフォルダに移動", @@ -617,6 +618,7 @@ "contentRating": { "title": "コンテンツレーティングを設定", "current": "現在", + "multiple": "複数の値", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "{count} モデルのベースモデルが正常に更新されました", "bulkBaseModelUpdatePartial": "{success} モデルを更新、{failed} モデルは失敗しました", "bulkBaseModelUpdateFailed": "選択したモデルのベースモデルの更新に失敗しました", + "bulkContentRatingUpdating": "{count} 件のモデルのコンテンツレーティングを更新中...", + "bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました", + "bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました", + "bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした", "invalidCharactersRemoved": "ファイル名から無効な文字が削除されました", "filenameCannotBeEmpty": "ファイル名を空にすることはできません", "renameFailed": "ファイル名の変更に失敗しました:{message}", diff --git a/locales/ko.json b/locales/ko.json index da128cc2..b99347ee 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -393,6 +393,7 @@ "viewSelected": "선택 항목 보기", "addTags": "모두에 태그 추가", "setBaseModel": "모두에 베이스 모델 설정", + "setContentRating": "모든 모델에 콘텐츠 등급 설정", "copyAll": "모든 문법 복사", "refreshAll": "모든 메타데이터 새로고침", "moveAll": "모두 폴더로 이동", @@ -617,6 +618,7 @@ "contentRating": { "title": "콘텐츠 등급 설정", "current": "현재", + "multiple": "여러 값", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "{count}개의 모델에 베이스 모델이 성공적으로 업데이트되었습니다", "bulkBaseModelUpdatePartial": "{success}개의 모델이 업데이트되었고, {failed}개의 모델이 실패했습니다", "bulkBaseModelUpdateFailed": "선택한 모델의 베이스 모델 업데이트에 실패했습니다", + "bulkContentRatingUpdating": "{count}개 모델의 콘텐츠 등급을 업데이트하는 중...", + "bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다", + "bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다", + "bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다", "invalidCharactersRemoved": "파일명에서 잘못된 문자가 제거되었습니다", "filenameCannotBeEmpty": "파일 이름은 비어있을 수 없습니다", "renameFailed": "파일 이름 변경 실패: {message}", diff --git a/locales/ru.json b/locales/ru.json index 9f11fe94..f17c5f61 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -393,6 +393,7 @@ "viewSelected": "Просмотреть выбранные", "addTags": "Добавить теги ко всем", "setBaseModel": "Установить базовую модель для всех", + "setContentRating": "Установить рейтинг контента для всех", "copyAll": "Копировать весь синтаксис", "refreshAll": "Обновить все метаданные", "moveAll": "Переместить все в папку", @@ -617,6 +618,7 @@ "contentRating": { "title": "Установить рейтинг контента", "current": "Текущий", + "multiple": "Несколько значений", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "Базовая модель успешно обновлена для {count} моделей", "bulkBaseModelUpdatePartial": "Обновлено {success} моделей, не удалось обновить {failed} моделей", "bulkBaseModelUpdateFailed": "Не удалось обновить базовую модель для выбранных моделей", + "bulkContentRatingUpdating": "Обновление рейтинга контента для {count} модель(ей)...", + "bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)", + "bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось", + "bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей", "invalidCharactersRemoved": "Недопустимые символы удалены из имени файла", "filenameCannotBeEmpty": "Имя файла не может быть пустым", "renameFailed": "Не удалось переименовать файл: {message}", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index aad48214..60a90cb5 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -399,6 +399,7 @@ "viewSelected": "查看已选中", "addTags": "为所有添加标签", "setBaseModel": "为所有设置基础模型", + "setContentRating": "为全部设置内容评级", "copyAll": "复制全部语法", "refreshAll": "刷新全部元数据", "moveAll": "全部移动到文件夹", @@ -623,6 +624,7 @@ "contentRating": { "title": "设置内容评级", "current": "当前", + "multiple": "多个值", "levels": { "pg": "PG", "pg13": "PG13", @@ -1101,6 +1103,10 @@ "bulkBaseModelUpdateSuccess": "成功为 {count} 个模型更新基础模型", "bulkBaseModelUpdatePartial": "更新了 {success} 个模型,{failed} 个失败", "bulkBaseModelUpdateFailed": "为选中模型更新基础模型失败", + "bulkContentRatingUpdating": "正在为 {count} 个模型更新内容评级...", + "bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}", + "bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败", + "bulkContentRatingFailed": "未能更新所选模型的内容评级", "invalidCharactersRemoved": "文件名中的无效字符已移除", "filenameCannotBeEmpty": "文件名不能为空", "renameFailed": "重命名文件失败:{message}", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f2b8f468..5469d6ed 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -393,6 +393,7 @@ "viewSelected": "檢視已選取", "addTags": "新增標籤到全部", "setBaseModel": "設定全部基礎模型", + "setContentRating": "為全部設定內容分級", "copyAll": "複製全部語法", "refreshAll": "刷新全部 metadata", "moveAll": "全部移動到資料夾", @@ -617,6 +618,7 @@ "contentRating": { "title": "設定內容分級", "current": "目前", + "multiple": "多個值", "levels": { "pg": "PG", "pg13": "PG13", @@ -1095,6 +1097,10 @@ "bulkBaseModelUpdateSuccess": "已成功為 {count} 個模型更新基礎模型", "bulkBaseModelUpdatePartial": "已更新 {success} 個模型,{failed} 個模型失敗", "bulkBaseModelUpdateFailed": "更新所選模型的基礎模型失敗", + "bulkContentRatingUpdating": "正在為 {count} 個模型更新內容分級...", + "bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}", + "bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗", + "bulkContentRatingFailed": "無法更新所選模型的內容分級", "invalidCharactersRemoved": "已移除檔名中的無效字元", "filenameCannotBeEmpty": "檔案名稱不可為空", "renameFailed": "重新命名檔案失敗:{message}", diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 638b803f..0428adbe 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -28,6 +28,7 @@ export class BulkContextMenu extends BaseContextMenu { // Update button visibility based on model type const addTagsItem = this.menu.querySelector('[data-action="add-tags"]'); const setBaseModelItem = this.menu.querySelector('[data-action="set-base-model"]'); + const setContentRatingItem = this.menu.querySelector('[data-action="set-content-rating"]'); const sendToWorkflowAppendItem = this.menu.querySelector('[data-action="send-to-workflow-append"]'); const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]'); const copyAllItem = this.menu.querySelector('[data-action="copy-all"]'); @@ -63,6 +64,9 @@ export class BulkContextMenu extends BaseContextMenu { if (setBaseModelItem) { setBaseModelItem.style.display = 'flex'; // Base model editing is available for all model types } + if (setContentRatingItem) { + setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none'; + } } updateSelectedCountHeader() { @@ -86,6 +90,9 @@ export class BulkContextMenu extends BaseContextMenu { case 'set-base-model': bulkManager.showBulkBaseModelModal(); break; + case 'set-content-rating': + bulkManager.showBulkContentRatingSelector(); + break; case 'send-to-workflow-append': bulkManager.sendAllModelsToWorkflow(false); break; diff --git a/static/js/components/ContextMenu/ModelContextMenuMixin.js b/static/js/components/ContextMenu/ModelContextMenuMixin.js index 3c461a9a..1c2c0a9a 100644 --- a/static/js/components/ContextMenu/ModelContextMenuMixin.js +++ b/static/js/components/ContextMenu/ModelContextMenuMixin.js @@ -2,6 +2,7 @@ import { showToast, getNSFWLevelName, openExampleImagesFolder } from '../../util import { modalManager } from '../../managers/ModalManager.js'; import { state } from '../../state/index.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; +import { bulkManager } from '../../managers/BulkManager.js'; // Mixin with shared functionality for LoraContextMenu and CheckpointContextMenu export const ModelContextMenuMixin = { @@ -11,6 +12,7 @@ export const ModelContextMenuMixin = { const closeBtn = this.nsfwSelector.querySelector('.close-nsfw-selector'); closeBtn.addEventListener('click', () => { this.nsfwSelector.style.display = 'none'; + this.resetNSFWSelectorState(); }); // Level buttons @@ -18,41 +20,70 @@ export const ModelContextMenuMixin = { levelButtons.forEach(btn => { btn.addEventListener('click', async () => { const level = parseInt(btn.dataset.level); + const mode = this.nsfwSelector.dataset.mode || 'single'; + + if (mode === 'bulk') { + let bulkFilePaths = []; + if (this.nsfwSelector.dataset.bulkFilePaths) { + try { + bulkFilePaths = JSON.parse(this.nsfwSelector.dataset.bulkFilePaths); + } catch (error) { + console.warn('Failed to parse bulk file paths for content rating', error); + } + } + + const success = await bulkManager.setBulkContentRating(level, bulkFilePaths); + if (success) { + this.nsfwSelector.style.display = 'none'; + this.resetNSFWSelectorState(); + } + return; + } + const filePath = this.nsfwSelector.dataset.cardPath; - + if (!filePath) return; - + try { await this.saveModelMetadata(filePath, { preview_nsfw_level: level }); - + showToast('toast.contextMenu.contentRatingSet', { level: getNSFWLevelName(level) }, 'success'); this.nsfwSelector.style.display = 'none'; + this.resetNSFWSelectorState(); } catch (error) { showToast('toast.contextMenu.contentRatingFailed', { message: error.message }, 'error'); } }); }); - + // Close when clicking outside document.addEventListener('click', (e) => { - if (this.nsfwSelector.style.display === 'block' && - !this.nsfwSelector.contains(e.target) && - !e.target.closest('.context-menu-item[data-action="set-nsfw"]')) { + if (this.nsfwSelector.style.display === 'block' && + !this.nsfwSelector.contains(e.target) && + !e.target.closest('.context-menu-item[data-action="set-nsfw"], .context-menu-item[data-action="set-content-rating"]')) { this.nsfwSelector.style.display = 'none'; + this.resetNSFWSelectorState(); } }); }, + resetNSFWSelectorState() { + if (!this.nsfwSelector) return; + delete this.nsfwSelector.dataset.bulkFilePaths; + delete this.nsfwSelector.dataset.mode; + delete this.nsfwSelector.dataset.cardPath; + }, + showNSFWLevelSelector(x, y, card) { const selector = document.getElementById('nsfwLevelSelector'); const currentLevelEl = document.getElementById('currentNSFWLevel'); - + // Get current NSFW level let currentLevel = 0; try { const metaData = JSON.parse(card.dataset.meta || '{}'); currentLevel = metaData.preview_nsfw_level || 0; - + // Update if we have no recorded level but have a dataset attribute if (!currentLevel && card.dataset.nsfwLevel) { currentLevel = parseInt(card.dataset.nsfwLevel) || 0; @@ -60,35 +91,37 @@ export const ModelContextMenuMixin = { } catch (err) { console.error('Error parsing metadata:', err); } - + currentLevelEl.textContent = getNSFWLevelName(currentLevel); - + // Position the selector if (x && y) { const viewportWidth = document.documentElement.clientWidth; const viewportHeight = document.documentElement.clientHeight; const selectorRect = selector.getBoundingClientRect(); - + // Center the selector if no coordinates provided let finalX = (viewportWidth - selectorRect.width) / 2; let finalY = (viewportHeight - selectorRect.height) / 2; - + selector.style.left = `${finalX}px`; selector.style.top = `${finalY}px`; } - + // Highlight current level button - document.querySelectorAll('.nsfw-level-btn').forEach(btn => { + selector.querySelectorAll('.nsfw-level-btn').forEach(btn => { if (parseInt(btn.dataset.level) === currentLevel) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); - + // Store reference to current card + selector.dataset.mode = 'single'; selector.dataset.cardPath = card.dataset.filepath; - + delete selector.dataset.bulkFilePaths; + // Show selector selector.style.display = 'block'; }, diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index f6fdf7c8..7f596a19 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,5 +1,5 @@ import { state, getCurrentPageState } from '../state/index.js'; -import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; @@ -35,7 +35,8 @@ export class BulkManager { refreshAll: true, moveAll: true, autoOrganize: true, - deleteAll: true + deleteAll: true, + setContentRating: true }, [MODEL_TYPES.EMBEDDING]: { addTags: true, @@ -44,7 +45,8 @@ export class BulkManager { refreshAll: true, moveAll: true, autoOrganize: true, - deleteAll: true + deleteAll: true, + setContentRating: false }, [MODEL_TYPES.CHECKPOINT]: { addTags: true, @@ -53,7 +55,8 @@ export class BulkManager { refreshAll: true, moveAll: false, autoOrganize: true, - deleteAll: true + deleteAll: true, + setContentRating: true } }; } @@ -850,20 +853,137 @@ export class BulkManager { showToast('toast.models.noModelsSelected', {}, 'warning'); return; } - + const countElement = document.getElementById('bulkBaseModelCount'); if (countElement) { countElement.textContent = state.selectedModels.size; } - + modalManager.showModal('bulkBaseModelModal', null, null, () => { this.cleanupBulkBaseModelModal(); }); - + // Initialize the bulk base model interface this.initializeBulkBaseModelInterface(); } - + + showBulkContentRatingSelector() { + if (state.selectedModels.size === 0) { + showToast('toast.models.noModelsSelected', {}, 'warning'); + return; + } + + const selector = document.getElementById('nsfwLevelSelector'); + const currentLevelEl = document.getElementById('currentNSFWLevel'); + + if (!selector || !currentLevelEl) { + console.warn('NSFW level selector not found'); + return; + } + + const filePaths = Array.from(state.selectedModels); + selector.dataset.mode = 'bulk'; + selector.dataset.bulkFilePaths = JSON.stringify(filePaths); + delete selector.dataset.cardPath; + + const selectedCards = Array.from(document.querySelectorAll('.model-card.selected')); + const levels = new Set(); + + selectedCards.forEach((card) => { + let level = 0; + try { + const metaData = JSON.parse(card.dataset.meta || '{}'); + if (typeof metaData.preview_nsfw_level === 'number') { + level = metaData.preview_nsfw_level; + } + } catch (error) { + console.warn('Failed to parse metadata for card', error); + } + + if (!level && card.dataset.nsfwLevel) { + const parsed = parseInt(card.dataset.nsfwLevel, 10); + if (!Number.isNaN(parsed)) { + level = parsed; + } + } + + levels.add(level); + }); + + let highlightLevel = null; + if (levels.size === 1) { + highlightLevel = levels.values().next().value; + currentLevelEl.textContent = getNSFWLevelName(highlightLevel); + } else { + currentLevelEl.textContent = translate('modals.contentRating.multiple', {}, 'Multiple values'); + } + + selector.querySelectorAll('.nsfw-level-btn').forEach((btn) => { + const btnLevel = parseInt(btn.dataset.level, 10); + if (highlightLevel !== null && btnLevel === highlightLevel) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; + const selectorRect = selector.getBoundingClientRect(); + const finalX = Math.max((viewportWidth - selectorRect.width) / 2, 0); + const finalY = Math.max((viewportHeight - selectorRect.height) / 2, 0); + + selector.style.left = `${finalX}px`; + selector.style.top = `${finalY}px`; + selector.style.display = 'block'; + } + + async setBulkContentRating(level, filePaths = null) { + const targets = Array.isArray(filePaths) ? filePaths : Array.from(state.selectedModels); + + if (!targets || targets.length === 0) { + showToast('toast.models.noModelsSelected', {}, 'warning'); + return false; + } + + const totalCount = targets.length; + const levelName = getNSFWLevelName(level); + + state.loadingManager.showSimpleLoading(translate('toast.models.bulkContentRatingUpdating', { count: totalCount })); + + let successCount = 0; + let failureCount = 0; + + try { + const apiClient = getModelApiClient(); + for (const filePath of targets) { + try { + await apiClient.saveModelMetadata(filePath, { preview_nsfw_level: level }); + successCount++; + } catch (error) { + failureCount++; + console.error(`Failed to set content rating for ${filePath}:`, error); + } + } + } finally { + state.loadingManager.hideSimpleLoading(); + } + + if (successCount === totalCount) { + showToast('toast.models.bulkContentRatingSet', { count: successCount, level: levelName }, 'success'); + } else if (successCount > 0) { + showToast('toast.models.bulkContentRatingPartial', { + success: successCount, + failed: failureCount, + level: levelName + }, 'warning'); + } else { + showToast('toast.models.bulkContentRatingFailed', {}, 'error'); + } + + return successCount > 0; + } + /** * Initialize bulk base model interface */ diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index a575e1a8..28e6ad57 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -56,6 +56,9 @@
+