mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(bulk): add bulk content rating action
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user