From 0cad6b5cbc5450a6c1d78738654e9861f2132b42 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 12 Mar 2025 21:06:31 +0800 Subject: [PATCH] Add nsfw browse control part 1 --- py/routes/api_routes.py | 4 + py/routes/lora_routes.py | 1 + py/services/download_manager.py | 1 + py/services/settings_manager.py | 3 +- py/utils/models.py | 2 + static/css/components/card.css | 90 +++++++++++++++++++ static/css/components/lora-modal.css | 36 ++++++++ static/css/components/modal.css | 120 ++++++++++++++++++++++++++ static/js/components/LoraCard.js | 83 +++++++++++++++++- static/js/components/LoraModal.js | 113 ++++++++++++++++++++++-- static/js/main.js | 5 +- static/js/managers/SettingsManager.js | 44 +++++++++- static/js/state/index.js | 32 ++++++- static/js/utils/constants.js | 9 ++ templates/components/modals.html | 34 ++++++++ 15 files changed, 561 insertions(+), 16 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 8e74a1c6..d7fd2bc9 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -200,6 +200,7 @@ class ApiRoutes: "model_name": lora["model_name"], "file_name": lora["file_name"], "preview_url": config.get_preview_static_url(lora["preview_url"]), + "preview_nsfw_level": lora.get("preview_nsfw_level", 0), "base_model": lora["base_model"], "folder": lora["folder"], "sha256": lora["sha256"], @@ -375,6 +376,7 @@ class ApiRoutes: if await client.download_preview_image(first_preview['url'], preview_path): local_metadata['preview_url'] = preview_path.replace(os.sep, '/') + local_metadata['preview_nsfw_level'] = first_preview.get('nsfwLevel', 0) # Save updated metadata with open(metadata_path, 'w', encoding='utf-8') as f: @@ -572,6 +574,8 @@ class ApiRoutes: # Validate and update settings if 'civitai_api_key' in data: settings.set('civitai_api_key', data['civitai_api_key']) + if 'show_only_sfw' in data: + settings.set('show_only_sfw', data['show_only_sfw']) return web.json_response({'success': True}) except Exception as e: diff --git a/py/routes/lora_routes.py b/py/routes/lora_routes.py index 5359aacc..586eb09f 100644 --- a/py/routes/lora_routes.py +++ b/py/routes/lora_routes.py @@ -26,6 +26,7 @@ class LoraRoutes: "model_name": lora["model_name"], "file_name": lora["file_name"], "preview_url": config.get_preview_static_url(lora["preview_url"]), + "preview_nsfw_level": lora.get("preview_nsfw_level", 0), "base_model": lora["base_model"], "folder": lora["folder"], "sha256": lora["sha256"], diff --git a/py/services/download_manager.py b/py/services/download_manager.py index f3a259cd..2dd0444f 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -96,6 +96,7 @@ class DownloadManager: preview_path = os.path.splitext(save_path)[0] + '.preview' + preview_ext if await self.civitai_client.download_preview_image(images[0]['url'], preview_path): metadata.preview_url = preview_path.replace(os.sep, '/') + metadata.preview_nsfw_level = images[0].get('nsfwLevel', 0) with open(metadata_path, 'w', encoding='utf-8') as f: json.dump(metadata.to_dict(), f, indent=2, ensure_ascii=False) diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 0f323e73..a003c2df 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -37,7 +37,8 @@ class SettingsManager: def _get_default_settings(self) -> Dict[str, Any]: """Return default settings""" return { - "civitai_api_key": "" + "civitai_api_key": "", + "show_only_sfw": False } def get(self, key: str, default: Any = None) -> Any: diff --git a/py/utils/models.py b/py/utils/models.py index 49568fee..67be65f7 100644 --- a/py/utils/models.py +++ b/py/utils/models.py @@ -15,6 +15,7 @@ class LoraMetadata: sha256: str # SHA256 hash of the file base_model: str # Base model (SD1.5/SD2.1/SDXL/etc.) preview_url: str # Preview image URL + preview_nsfw_level: int = 0 # NSFW level of the preview image usage_tips: str = "{}" # Usage tips for the model, json string notes: str = "" # Additional notes from_civitai: bool = True # Whether the lora is from Civitai @@ -49,6 +50,7 @@ class LoraMetadata: sha256=file_info['hashes'].get('SHA256', ''), base_model=base_model, preview_url=None, # Will be updated after preview download + preview_nsfw_level=0, # Will be updated after preview download, it is decided by the nsfw level of the preview image from_civitai=True, civitai=version_info ) diff --git a/static/css/components/card.css b/static/css/components/card.css index 99e272e8..84801ef3 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -60,6 +60,96 @@ object-position: center top; /* Align the top of the image with the top of the container */ } +/* NSFW Content Blur */ +.card-preview.blurred img, +.card-preview.blurred video { + filter: blur(25px); +} + +.nsfw-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + pointer-events: none; +} + +.nsfw-warning { + text-align: center; + color: white; + background: rgba(0, 0, 0, 0.6); + padding: var(--space-2); + border-radius: var(--border-radius-base); + backdrop-filter: blur(4px); + max-width: 80%; + pointer-events: auto; +} + +.nsfw-warning p { + margin: 0 0 var(--space-1); + font-weight: bold; + font-size: 1.1em; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +.toggle-blur-btn { + position: absolute; + left: var(--space-1); + top: var(--space-1); + background: rgba(0, 0, 0, 0.5); + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: white; + cursor: pointer; + z-index: 3; + transition: background-color 0.2s, transform 0.2s; +} + +.toggle-blur-btn:hover { + background: rgba(0, 0, 0, 0.7); + transform: scale(1.1); +} + +.toggle-blur-btn i { + font-size: 0.9em; +} + +.show-content-btn { + background: var(--lora-accent); + color: white; + border: none; + border-radius: var(--border-radius-xs); + padding: 4px var(--space-1); + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.2s, transform 0.2s; +} + +.show-content-btn:hover { + background: oklch(58% 0.28 256); + transform: scale(1.05); +} + +/* Adjust base model label positioning when toggle button is present */ +.base-model-label.with-toggle { + margin-left: 28px; /* Make room for the toggle button */ +} + +/* Ensure card actions remain clickable */ +.card-header .card-actions { + z-index: 3; +} + .card-footer { position: absolute; bottom: 0; diff --git a/static/css/components/lora-modal.css b/static/css/components/lora-modal.css index df2bd828..16250e94 100644 --- a/static/css/components/lora-modal.css +++ b/static/css/components/lora-modal.css @@ -998,4 +998,40 @@ [data-theme="dark"] .tooltip-tag { background: rgba(255, 255, 255, 0.03); border: 1px solid var(--lora-border); +} + +/* Add styles for blurred showcase content */ +.nsfw-media-wrapper { + position: relative; +} + +.media-wrapper img.blurred, +.media-wrapper video.blurred { + filter: blur(25px); +} + +.media-wrapper .nsfw-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + pointer-events: none; +} + +/* Position the toggle button at the top left of showcase media */ +.showcase-toggle-btn { + position: absolute; + left: var(--space-1); + top: var(--space-1); + z-index: 3; +} + +/* Make sure media wrapper maintains position: relative for absolute positioning of children */ +.carousel .media-wrapper { + position: relative; } \ No newline at end of file diff --git a/static/css/components/modal.css b/static/css/components/modal.css index 10c7c141..a8684865 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -323,4 +323,124 @@ body.modal-open { [data-theme="dark"] .path-preview { background: rgba(255, 255, 255, 0.03); border: 1px solid var(--lora-border); +} + +/* Settings Styles */ +.settings-section { + margin-top: var(--space-3); + border-top: 1px solid var(--lora-border); + padding-top: var(--space-2); +} + +.settings-section h3 { + font-size: 1.1em; + margin-bottom: var(--space-2); + color: var(--text-color); + opacity: 0.9; +} + +.setting-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: var(--space-2); + padding: var(--space-1); + border-radius: var(--border-radius-xs); +} + +.setting-item:hover { + background: rgba(0, 0, 0, 0.02); +} + +[data-theme="dark"] .setting-item:hover { + background: rgba(255, 255, 255, 0.05); +} + +.setting-info { + flex: 1; +} + +.setting-info label { + display: block; + margin-bottom: 4px; + font-weight: 500; +} + +.setting-control { + padding-left: var(--space-2); +} + +/* Toggle Switch */ +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + cursor: pointer; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--border-color); + transition: .3s; + border-radius: 24px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .3s; + border-radius: 50%; +} + +input:checked + .toggle-slider { + background-color: var(--lora-accent); +} + +input:checked + .toggle-slider:before { + transform: translateX(26px); +} + +.toggle-label { + margin-left: 60px; + line-height: 24px; +} + +/* Add small animation for the toggle */ +.toggle-slider:active:before { + width: 22px; +} + +/* Update input help styles */ +.input-help { + font-size: 0.85em; + color: var(--text-color); + opacity: 0.7; + margin-top: 4px; + line-height: 1.4; +} + +/* Blur effect for NSFW content */ +.nsfw-blur { + filter: blur(12px); + transition: filter 0.3s ease; +} + +.nsfw-blur:hover { + filter: blur(8px); } \ No newline at end of file diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index 09c0b96f..f266f662 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -2,6 +2,7 @@ import { showToast } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showLoraModal } from './LoraModal.js'; import { bulkManager } from '../managers/BulkManager.js'; +import { NSFW_LEVELS } from '../utils/constants.js'; export function createLoraCard(lora) { const card = document.createElement('div'); @@ -27,6 +28,16 @@ export function createLoraCard(lora) { card.dataset.modelDescription = lora.modelDescription; } + // Store NSFW level if available + const nsfwLevel = lora.preview_nsfw_level !== undefined ? lora.preview_nsfw_level : 0; + card.dataset.nsfwLevel = nsfwLevel; + + // Determine if the preview should be blurred based on NSFW level and user settings + const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13; + if (shouldBlur) { + card.classList.add('nsfw-content'); + } + // Apply selection state if in bulk mode and this card is in the selected set if (state.bulkMode && state.selectedLoras.has(lora.file_path)) { card.classList.add('selected'); @@ -36,8 +47,18 @@ export function createLoraCard(lora) { const previewUrl = lora.preview_url || '/loras_static/images/no-preview.png'; const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl; + // Determine NSFW warning text based on level + let nsfwText = "Mature Content"; + if (nsfwLevel >= NSFW_LEVELS.XXX) { + nsfwText = "XXX-rated Content"; + } else if (nsfwLevel >= NSFW_LEVELS.X) { + nsfwText = "X-rated Content"; + } else if (nsfwLevel >= NSFW_LEVELS.R) { + nsfwText = "R-rated Content"; + } + card.innerHTML = ` -