mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(showcase): add Show button to NSFW notice in main media area
- Add showcase__nsfw-notice-content wrapper for better layout - Add showcase__nsfw-show-btn with styling matching card.css show-content-btn - Add show-content action handler that triggers global blur toggle - Button uses blue accent color with eye icon and hover effects - Clicking Show button syncs with blur toggle button icon state - Use unique class names to avoid conflicts with card.css
This commit is contained in:
@@ -65,6 +65,8 @@ body {
|
||||
--space-1: calc(8px * 1);
|
||||
--space-2: calc(8px * 2);
|
||||
--space-3: calc(8px * 3);
|
||||
--space-4: calc(8px * 4);
|
||||
--space-5: calc(8px * 5);
|
||||
|
||||
/* Z-index Scale */
|
||||
--z-base: 10;
|
||||
|
||||
@@ -44,6 +44,128 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Media container for images and videos */
|
||||
.showcase__media-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.showcase-media-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.showcase__media-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.showcase__media {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.showcase__media.blurred {
|
||||
filter: blur(25px);
|
||||
}
|
||||
|
||||
/* NSFW notice for main media - redesigned to avoid conflicts with card.css */
|
||||
.showcase__nsfw-notice {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
z-index: 5;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.showcase__nsfw-notice-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.showcase__nsfw-notice-text {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Show content button in NSFW notice - styled like card.css show-content-btn */
|
||||
.showcase__nsfw-show-btn {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 6px var(--space-3);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.showcase__nsfw-show-btn:hover {
|
||||
background: oklch(58% 0.28 256);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.showcase__nsfw-show-btn i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Control button active state for blur toggle */
|
||||
.showcase__control-btn.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Video indicator for thumbnails */
|
||||
.thumbnail-rail__video-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* NSFW blur for thumbnails */
|
||||
.thumbnail-rail__item.nsfw-blur img {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Navigation arrows */
|
||||
.showcase__nav {
|
||||
position: absolute;
|
||||
@@ -133,6 +255,16 @@
|
||||
background: var(--lora-error);
|
||||
}
|
||||
|
||||
/* Active state for toggle buttons */
|
||||
.showcase__control-btn.active {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.showcase__control-btn.active:hover {
|
||||
background: var(--lora-accent-hover, #3182ce);
|
||||
}
|
||||
|
||||
/* Params panel (slide up) */
|
||||
.showcase__params {
|
||||
position: absolute;
|
||||
|
||||
@@ -55,7 +55,12 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* NSFW blur */
|
||||
/* NSFW blur for thumbnails - BEM naming to avoid conflicts with global .nsfw-blur */
|
||||
.thumbnail-rail__item--nsfw-blurred img {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Legacy support for old class names (deprecated) */
|
||||
.thumbnail-rail__item.nsfw img {
|
||||
filter: blur(8px);
|
||||
}
|
||||
@@ -72,6 +77,8 @@
|
||||
border-radius: var(--border-radius-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
|
||||
@@ -12,6 +12,9 @@ import { escapeHtml } from '../shared/utils.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||
import { getNsfwLevelSelector } from '../shared/NsfwLevelSelector.js';
|
||||
|
||||
export class Showcase {
|
||||
constructor(container) {
|
||||
@@ -23,18 +26,26 @@ export class Showcase {
|
||||
this.paramsVisible = false;
|
||||
this.uploadAreaVisible = false;
|
||||
this.isUploading = false;
|
||||
this.localFiles = [];
|
||||
this.globalBlurEnabled = true; // Will be initialized based on user settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the showcase
|
||||
*/
|
||||
render({ images, modelHash, filePath }) {
|
||||
async render({ images, modelHash, filePath }) {
|
||||
this.images = images || [];
|
||||
this.modelHash = modelHash || '';
|
||||
this.filePath = filePath || '';
|
||||
this.currentIndex = 0;
|
||||
this.paramsVisible = false;
|
||||
this.uploadAreaVisible = false;
|
||||
|
||||
// Initialize global blur state based on user settings
|
||||
this.globalBlurEnabled = state.settings.blur_mature_content;
|
||||
|
||||
// Fetch local example files
|
||||
await this.fetchLocalFiles();
|
||||
|
||||
this.element.innerHTML = this.getTemplate();
|
||||
this.bindEvents();
|
||||
@@ -44,6 +55,106 @@ export class Showcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch local example files from the server
|
||||
*/
|
||||
async fetchLocalFiles() {
|
||||
if (!this.modelHash) {
|
||||
this.localFiles = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/lm/example-image-files?model_hash=${this.modelHash}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.localFiles = result.files || [];
|
||||
} else {
|
||||
this.localFiles = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch local example files:', error);
|
||||
this.localFiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the matching local file for an image
|
||||
* @param {Object} img - Image metadata
|
||||
* @param {number} index - Image index
|
||||
* @returns {Object|null} Matching local file or null
|
||||
*/
|
||||
findLocalFile(img, index) {
|
||||
if (!this.localFiles || this.localFiles.length === 0) return null;
|
||||
|
||||
let localFile = null;
|
||||
|
||||
if (typeof img.id === 'string' && img.id) {
|
||||
// This is a custom image, find by custom_<id>
|
||||
const customPrefix = `custom_${img.id}`;
|
||||
localFile = this.localFiles.find(file => file.name.startsWith(customPrefix));
|
||||
} else {
|
||||
// This is a regular image from civitai, find by index
|
||||
localFile = this.localFiles.find(file => {
|
||||
const match = file.name.match(/image_(\d+)\./);
|
||||
return match && parseInt(match[1]) === index;
|
||||
});
|
||||
}
|
||||
|
||||
return localFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if media is a video
|
||||
* @param {Object} img - Image metadata
|
||||
* @param {Object} localFile - Local file object
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVideo(img, localFile) {
|
||||
if (localFile) {
|
||||
return localFile.is_video;
|
||||
}
|
||||
const url = img.url || '';
|
||||
return url.endsWith('.mp4') || url.endsWith('.webm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content should be blurred based on NSFW settings
|
||||
* @param {number} nsfwLevel - NSFW level of the content
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldBlurContent(nsfwLevel) {
|
||||
return this.globalBlurEnabled && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any image in the showcase is NSFW
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasNsfwContent() {
|
||||
return this.images.some(img => {
|
||||
const level = img.nsfwLevel !== undefined ? img.nsfwLevel : 0;
|
||||
return level > NSFW_LEVELS.PG13;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NSFW warning text based on level
|
||||
* @param {number} nsfwLevel - NSFW level
|
||||
* @returns {string}
|
||||
*/
|
||||
getNsfwText(nsfwLevel) {
|
||||
if (nsfwLevel >= NSFW_LEVELS.XXX) {
|
||||
return 'XXX-rated Content';
|
||||
} else if (nsfwLevel >= NSFW_LEVELS.X) {
|
||||
return 'X-rated Content';
|
||||
} else if (nsfwLevel >= NSFW_LEVELS.R) {
|
||||
return 'R-rated Content';
|
||||
}
|
||||
return 'Mature Content';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML template
|
||||
*/
|
||||
@@ -54,15 +165,25 @@ export class Showcase {
|
||||
<div class="showcase__main">
|
||||
${hasImages ? `
|
||||
<div class="showcase__image-wrapper">
|
||||
<img class="showcase__image" alt="${translate('modals.model.examples.title', {}, 'Example')}">
|
||||
<div class="showcase__media-container">
|
||||
<!-- Media will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div class="showcase__controls">
|
||||
<button class="showcase__control-btn ${this.hasNsfwContent() ? '' : 'hidden'}"
|
||||
data-action="toggle-global-blur"
|
||||
title="${this.globalBlurEnabled ? 'Show content' : 'Blur content'}">
|
||||
<i class="fas ${this.globalBlurEnabled ? 'fa-eye' : 'fa-eye-slash'}"></i>
|
||||
</button>
|
||||
<button class="showcase__control-btn" data-action="toggle-params" title="${translate('modals.model.actions.viewParams', {}, 'View parameters (I)')}">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
<button class="showcase__control-btn showcase__control-btn--primary" data-action="set-preview" title="${translate('modals.model.actions.setPreview', {}, 'Set as preview')}">
|
||||
<i class="fas fa-image"></i>
|
||||
</button>
|
||||
<button class="showcase__control-btn" data-action="set-nsfw" title="${translate('modals.model.actions.setContentRating', {}, 'Set content rating')}">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</button>
|
||||
<button class="showcase__control-btn showcase__control-btn--danger" data-action="delete-example" title="${translate('modals.model.actions.delete', {}, 'Delete')}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
@@ -109,14 +230,28 @@ export class Showcase {
|
||||
*/
|
||||
renderThumbnailRail() {
|
||||
const thumbnails = this.images.map((img, index) => {
|
||||
const url = img.url || img;
|
||||
const isNsfw = img.nsfw || false;
|
||||
const localFile = this.findLocalFile(img, index);
|
||||
const remoteUrl = img.url || img;
|
||||
const localUrl = localFile ? localFile.path : '';
|
||||
const url = localUrl || remoteUrl;
|
||||
const nsfwLevel = img.nsfwLevel !== undefined ? img.nsfwLevel : 0;
|
||||
// Check if this specific image needs blur based on global state
|
||||
const needsBlur = nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = this.globalBlurEnabled && needsBlur;
|
||||
const isVideo = this.isVideo(img, localFile);
|
||||
|
||||
return `
|
||||
<div class="thumbnail-rail__item ${index === 0 ? 'active' : ''} ${isNsfw ? 'nsfw' : ''}"
|
||||
<div class="thumbnail-rail__item ${index === 0 ? 'active' : ''} ${shouldBlur ? 'thumbnail-rail__item--nsfw-blurred' : ''}"
|
||||
data-index="${index}"
|
||||
data-action="select-image">
|
||||
<img src="${url}" loading="lazy" alt="">
|
||||
${isNsfw ? '<span class="thumbnail-rail__nsfw-badge">NSFW</span>' : ''}
|
||||
data-action="select-image"
|
||||
data-nsfw-level="${nsfwLevel}">
|
||||
${isVideo ? `
|
||||
<div class="thumbnail-rail__video-indicator">
|
||||
<i class="fas fa-play-circle"></i>
|
||||
</div>
|
||||
` : ''}
|
||||
<img src="${url}" loading="lazy" alt="" ${shouldBlur ? 'class="blurred"' : ''}>
|
||||
${shouldBlur ? '<span class="thumbnail-rail__nsfw-badge">NSFW</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -193,6 +328,9 @@ export class Showcase {
|
||||
case 'set-preview':
|
||||
this.setAsPreview();
|
||||
break;
|
||||
case 'set-nsfw':
|
||||
this.setContentRating();
|
||||
break;
|
||||
case 'delete-example':
|
||||
this.deleteExample();
|
||||
break;
|
||||
@@ -206,6 +344,13 @@ export class Showcase {
|
||||
case 'copy-prompt':
|
||||
this.copyPrompt();
|
||||
break;
|
||||
case 'toggle-blur':
|
||||
this.toggleBlur();
|
||||
break;
|
||||
case 'show-content':
|
||||
case 'toggle-global-blur':
|
||||
this.toggleGlobalBlur();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -374,8 +519,12 @@ export class Showcase {
|
||||
this.images = allImages;
|
||||
this.currentIndex = allImages.length - 1;
|
||||
|
||||
// Re-fetch local files
|
||||
await this.fetchLocalFiles();
|
||||
|
||||
// Re-render
|
||||
this.render({ images: allImages, modelHash: this.modelHash, filePath: this.filePath });
|
||||
this.element.innerHTML = this.getTemplate();
|
||||
this.bindEvents();
|
||||
|
||||
// Load the newly uploaded image
|
||||
if (this.currentIndex >= 0) {
|
||||
@@ -395,16 +544,26 @@ export class Showcase {
|
||||
|
||||
this.currentIndex = index;
|
||||
const image = this.images[index];
|
||||
const url = image.url || image;
|
||||
const localFile = this.findLocalFile(image, index);
|
||||
const remoteUrl = image.url || image;
|
||||
const localUrl = localFile ? localFile.path : '';
|
||||
const url = localUrl || remoteUrl;
|
||||
const nsfwLevel = image.nsfwLevel !== undefined ? image.nsfwLevel : 0;
|
||||
const shouldBlur = this.shouldBlurContent(nsfwLevel);
|
||||
const isVideo = this.isVideo(image, localFile);
|
||||
const nsfwText = this.getNsfwText(nsfwLevel);
|
||||
|
||||
// Update main image
|
||||
const imgElement = this.element.querySelector('.showcase__image');
|
||||
if (imgElement) {
|
||||
imgElement.classList.add('loading');
|
||||
imgElement.src = url;
|
||||
imgElement.onload = () => {
|
||||
imgElement.classList.remove('loading');
|
||||
};
|
||||
// Update main media container
|
||||
const mediaContainer = this.element.querySelector('.showcase__media-container');
|
||||
if (mediaContainer) {
|
||||
mediaContainer.innerHTML = this.renderMediaElement(url, isVideo, shouldBlur, nsfwText, nsfwLevel);
|
||||
}
|
||||
|
||||
// Update global blur toggle button visibility
|
||||
const blurToggleBtn = this.element.querySelector('[data-action="toggle-global-blur"]');
|
||||
if (blurToggleBtn) {
|
||||
const hasNsfw = this.hasNsfwContent();
|
||||
blurToggleBtn.classList.toggle('hidden', !hasNsfw);
|
||||
}
|
||||
|
||||
// Update thumbnail rail active state
|
||||
@@ -416,6 +575,125 @@ export class Showcase {
|
||||
this.updateParams(image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render media element (image or video)
|
||||
*/
|
||||
renderMediaElement(url, isVideo, shouldBlur, nsfwText, nsfwLevel) {
|
||||
const blurClass = shouldBlur ? 'blurred' : '';
|
||||
|
||||
const mediaHtml = isVideo ? `
|
||||
<video controls autoplay muted loop crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
class="showcase__media ${blurClass}">
|
||||
<source src="${url}" type="video/mp4">
|
||||
Your browser does not support video playback
|
||||
</video>
|
||||
` : `
|
||||
<img src="${url}"
|
||||
alt="${translate('modals.model.examples.title', {}, 'Example')}"
|
||||
class="showcase__media ${blurClass}">
|
||||
`;
|
||||
|
||||
const nsfwOverlay = shouldBlur ? `
|
||||
<div class="showcase__nsfw-notice">
|
||||
<div class="showcase__nsfw-notice-content">
|
||||
<p class="showcase__nsfw-notice-text">${nsfwText}</p>
|
||||
<button class="showcase__nsfw-show-btn" data-action="show-content">
|
||||
<i class="fas fa-eye"></i> ${translate('common.show', {}, 'Show')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="showcase-media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" data-nsfw-level="${nsfwLevel}">
|
||||
<div class="showcase__media-inner">
|
||||
${mediaHtml}
|
||||
${nsfwOverlay}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle global blur state for all images in the showcase
|
||||
*/
|
||||
toggleGlobalBlur() {
|
||||
this.globalBlurEnabled = !this.globalBlurEnabled;
|
||||
|
||||
// Update the toggle button
|
||||
const toggleBtn = this.element.querySelector('[data-action="toggle-global-blur"]');
|
||||
if (toggleBtn) {
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
if (icon) {
|
||||
icon.className = `fas ${this.globalBlurEnabled ? 'fa-eye' : 'fa-eye-slash'}`;
|
||||
}
|
||||
toggleBtn.title = this.globalBlurEnabled ? 'Show content' : 'Blur content';
|
||||
}
|
||||
|
||||
// Update main image
|
||||
this.updateMainImageBlur();
|
||||
|
||||
// Update all thumbnails
|
||||
this.updateAllThumbnailsBlur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update main image blur state based on global setting
|
||||
*/
|
||||
updateMainImageBlur() {
|
||||
const mediaContainer = this.element.querySelector('.showcase__media-container');
|
||||
if (!mediaContainer) return;
|
||||
|
||||
const media = mediaContainer.querySelector('.showcase__media');
|
||||
const overlay = mediaContainer.querySelector('.showcase__nsfw-notice');
|
||||
const currentImage = this.images[this.currentIndex];
|
||||
|
||||
if (!media || !currentImage) return;
|
||||
|
||||
const nsfwLevel = currentImage.nsfwLevel !== undefined ? currentImage.nsfwLevel : 0;
|
||||
const shouldBlur = this.globalBlurEnabled && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
|
||||
media.classList.toggle('blurred', shouldBlur);
|
||||
if (overlay) {
|
||||
overlay.style.display = shouldBlur ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all thumbnails blur state based on global setting
|
||||
*/
|
||||
updateAllThumbnailsBlur() {
|
||||
this.element.querySelectorAll('.thumbnail-rail__item').forEach((item, index) => {
|
||||
const img = item.querySelector('img');
|
||||
const badge = item.querySelector('.thumbnail-rail__nsfw-badge');
|
||||
const image = this.images[index];
|
||||
|
||||
if (!image) return;
|
||||
|
||||
const nsfwLevel = image.nsfwLevel !== undefined ? image.nsfwLevel : 0;
|
||||
const needsBlur = nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = this.globalBlurEnabled && needsBlur;
|
||||
|
||||
// Update item class
|
||||
item.classList.toggle('thumbnail-rail__item--nsfw-blurred', shouldBlur);
|
||||
|
||||
// Update image blur
|
||||
if (img) {
|
||||
img.classList.toggle('blurred', shouldBlur);
|
||||
}
|
||||
|
||||
// Update badge visibility
|
||||
if (needsBlur) {
|
||||
if (shouldBlur && !badge) {
|
||||
item.insertAdjacentHTML('beforeend', '<span class="thumbnail-rail__nsfw-badge">NSFW</span>');
|
||||
} else if (!shouldBlur && badge) {
|
||||
badge.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to previous image
|
||||
*/
|
||||
@@ -559,11 +837,20 @@ export class Showcase {
|
||||
const image = this.images[this.currentIndex];
|
||||
if (!image || !this.filePath) return;
|
||||
|
||||
const url = image.url || image;
|
||||
const localFile = this.findLocalFile(image, this.currentIndex);
|
||||
const remoteUrl = image.url || image;
|
||||
const localUrl = localFile ? localFile.path : '';
|
||||
const url = localUrl || remoteUrl;
|
||||
const nsfwLevel = image.nsfwLevel !== undefined ? image.nsfwLevel : 0;
|
||||
|
||||
try {
|
||||
// Fetch the image/video as a blob
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
||||
|
||||
const client = getModelApiClient();
|
||||
await client.setModelPreview(this.filePath, url);
|
||||
await client.uploadPreview(this.filePath, file, nsfwLevel);
|
||||
|
||||
showToast('modals.model.actions.previewSet', {}, 'success');
|
||||
} catch (err) {
|
||||
@@ -572,6 +859,88 @@ export class Showcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content rating for current example
|
||||
*/
|
||||
async setContentRating() {
|
||||
const image = this.images[this.currentIndex];
|
||||
if (!image || !this.modelHash) return;
|
||||
|
||||
const selector = getNsfwLevelSelector();
|
||||
if (!selector) {
|
||||
console.warn('NSFW selector not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLevel = image.nsfwLevel !== undefined ? image.nsfwLevel : 0;
|
||||
const isCustomImage = Boolean(typeof image.id === 'string' && image.id);
|
||||
const mediaIndex = this.currentIndex;
|
||||
const mediaId = image.id || '';
|
||||
|
||||
selector.show({
|
||||
currentLevel,
|
||||
onSelect: async (level) => {
|
||||
try {
|
||||
const payload = {
|
||||
model_hash: this.modelHash,
|
||||
nsfw_level: level,
|
||||
source: isCustomImage ? 'custom' : 'civitai',
|
||||
};
|
||||
|
||||
if (isCustomImage) {
|
||||
payload.id = mediaId;
|
||||
} else {
|
||||
payload.index = mediaIndex;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/lm/example-images/set-nsfw-level', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update NSFW level');
|
||||
}
|
||||
|
||||
// Update the image in our local array
|
||||
image.nsfwLevel = level;
|
||||
|
||||
// Re-render to apply the new blur state
|
||||
this.loadImage(this.currentIndex);
|
||||
|
||||
// Update thumbnail rail to reflect the change
|
||||
const thumbnail = this.element.querySelector(`.thumbnail-rail__item[data-index="${this.currentIndex}"]`);
|
||||
if (thumbnail) {
|
||||
const shouldBlur = this.shouldBlurContent(level);
|
||||
thumbnail.classList.toggle('thumbnail-rail__item--nsfw-blurred', shouldBlur);
|
||||
thumbnail.dataset.nsfwLevel = level;
|
||||
const thumbImg = thumbnail.querySelector('img');
|
||||
if (thumbImg) {
|
||||
thumbImg.classList.toggle('blurred', shouldBlur);
|
||||
}
|
||||
const badge = thumbnail.querySelector('.thumbnail-rail__nsfw-badge');
|
||||
if (shouldBlur && !badge) {
|
||||
thumbnail.insertAdjacentHTML('beforeend', '<span class="thumbnail-rail__nsfw-badge">NSFW</span>');
|
||||
} else if (!shouldBlur && badge) {
|
||||
badge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
showToast('toast.contextMenu.contentRatingSet', { level }, 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating NSFW level:', error);
|
||||
showToast('toast.contextMenu.contentRatingFailed', { message: error.message }, 'error');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete current example
|
||||
*/
|
||||
@@ -610,7 +979,8 @@ export class Showcase {
|
||||
}
|
||||
|
||||
// Re-render
|
||||
this.render({ images: this.images, modelHash: this.modelHash, filePath: this.filePath });
|
||||
this.element.innerHTML = this.getTemplate();
|
||||
this.bindEvents();
|
||||
|
||||
if (this.images.length > 0) {
|
||||
this.loadImage(this.currentIndex);
|
||||
|
||||
Reference in New Issue
Block a user